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);
213 if ($circulator->use_booking) {
214 $circulator->is_res_checkin($circulator->is_checkin(1))
215 if $api =~ /reservation.return/ or (
216 $api =~ /checkin/ and $circulator->seems_like_reservation()
219 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
222 $circulator->is_renewal(1) if $api =~ /renew/;
223 $circulator->is_checkin(1) if $api =~ /checkin/;
225 $circulator->mk_env();
226 $circulator->noop(1) if $circulator->claims_never_checked_out;
228 return circ_events($circulator) if $circulator->bail_out;
230 $circulator->override(1) if $api =~ /override/o;
232 if( $api =~ /checkout\.permit/ ) {
233 $circulator->do_permit();
235 } elsif( $api =~ /checkout.full/ ) {
237 # requesting a precat checkout implies that any required
238 # overrides have been performed. Go ahead and re-override.
239 $circulator->skip_permit_key(1);
240 $circulator->override(1) if $circulator->request_precat;
241 $circulator->do_permit();
242 $circulator->is_checkout(1);
243 unless( $circulator->bail_out ) {
244 $circulator->events([]);
245 $circulator->do_checkout();
248 } elsif( $circulator->is_res_checkout ) {
249 $circulator->do_reservation_pickup();
251 } elsif( $api =~ /inspect/ ) {
252 my $data = $circulator->do_inspect();
253 $circulator->editor->rollback;
256 } elsif( $api =~ /checkout/ ) {
257 $circulator->is_checkout(1);
258 $circulator->do_checkout();
260 } elsif( $circulator->is_res_checkin ) {
261 $circulator->do_reservation_return();
262 $circulator->do_checkin() if ($circulator->copy());
263 } elsif( $api =~ /checkin/ ) {
264 $circulator->do_checkin();
266 } elsif( $api =~ /renew/ ) {
267 $circulator->is_renewal(1);
268 $circulator->do_renew();
271 if( $circulator->bail_out ) {
274 # make sure no success event accidentally slip in
276 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
279 my @e = @{$circulator->events};
280 push( @ee, $_->{textcode} ) for @e;
281 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
283 $circulator->editor->rollback;
287 $circulator->editor->commit;
290 $conn->respond_complete(circ_events($circulator));
292 return undef if $circulator->bail_out;
294 $circulator->do_hold_notify($circulator->notify_hold)
295 if $circulator->notify_hold;
296 $circulator->retarget_holds if $circulator->retarget;
297 $circulator->append_reading_list;
298 $circulator->make_trigger_events;
305 my @e = @{$circ->events};
306 # if we have multiple events, SUCCESS should not be one of them;
307 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
308 return (@e == 1) ? $e[0] : \@e;
312 sub translate_legacy_args {
315 if( $$args{barcode} ) {
316 $$args{copy_barcode} = $$args{barcode};
317 delete $$args{barcode};
320 if( $$args{copyid} ) {
321 $$args{copy_id} = $$args{copyid};
322 delete $$args{copyid};
325 if( $$args{patronid} ) {
326 $$args{patron_id} = $$args{patronid};
327 delete $$args{patronid};
330 if( $$args{patron} and !ref($$args{patron}) ) {
331 $$args{patron_id} = $$args{patron};
332 delete $$args{patron};
336 if( $$args{noncat} ) {
337 $$args{is_noncat} = $$args{noncat};
338 delete $$args{noncat};
341 if( $$args{precat} ) {
342 $$args{is_precat} = $$args{request_precat} = $$args{precat};
343 delete $$args{precat};
349 # --------------------------------------------------------------------------
350 # This package actually manages all of the circulation logic
351 # --------------------------------------------------------------------------
352 package OpenILS::Application::Circ::Circulator;
353 use strict; use warnings;
354 use vars q/$AUTOLOAD/;
356 use OpenILS::Utils::Fieldmapper;
357 use OpenSRF::Utils::Cache;
358 use Digest::MD5 qw(md5_hex);
359 use DateTime::Format::ISO8601;
360 use OpenILS::Utils::PermitHold;
361 use OpenSRF::Utils qw/:datetime/;
362 use OpenSRF::Utils::SettingsClient;
363 use OpenILS::Application::Circ::Holds;
364 use OpenILS::Application::Circ::Transit;
365 use OpenSRF::Utils::Logger qw(:logger);
366 use OpenILS::Utils::CStoreEditor qw/:funcs/;
367 use OpenILS::Const qw/:const/;
368 use OpenILS::Utils::Penalty;
369 use OpenILS::Application::Circ::CircCommon;
372 my $CC = "OpenILS::Application::Circ::CircCommon";
373 my $holdcode = "OpenILS::Application::Circ::Holds";
374 my $transcode = "OpenILS::Application::Circ::Transit";
380 # --------------------------------------------------------------------------
381 # Add a pile of automagic getter/setter methods
382 # --------------------------------------------------------------------------
383 my @AUTOLOAD_FIELDS = qw/
429 recurring_fines_level
442 cancelled_hold_transit
449 circ_matrix_matchpoint
460 claims_never_checked_out
473 dont_change_lost_zero
475 needs_lost_bill_handling
481 my $type = ref($self) or die "$self is not an object";
483 my $name = $AUTOLOAD;
486 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
487 $logger->error("circulator: $type: invalid autoload field: $name");
488 die "$type: invalid autoload field: $name\n"
493 *{"${type}::${name}"} = sub {
496 $s->{$name} = $v if defined $v;
500 return $self->$name($data);
505 my( $class, $auth, %args ) = @_;
506 $class = ref($class) || $class;
507 my $self = bless( {}, $class );
510 $self->editor(new_editor(xact => 1, authtoken => $auth));
512 unless( $self->editor->checkauth ) {
513 $self->bail_on_events($self->editor->event);
517 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
519 $self->$_($args{$_}) for keys %args;
522 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
524 # if this is a renewal, default to desk_renewal
525 $self->desk_renewal(1) unless
526 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
528 $self->capture('') unless $self->capture;
530 unless(%user_groups) {
531 my $gps = $self->editor->retrieve_all_permission_grp_tree;
532 %user_groups = map { $_->id => $_ } @$gps;
539 # --------------------------------------------------------------------------
540 # True if we should discontinue processing
541 # --------------------------------------------------------------------------
543 my( $self, $bool ) = @_;
544 if( defined $bool ) {
545 $logger->info("circulator: BAILING OUT") if $bool;
546 $self->{bail_out} = $bool;
548 return $self->{bail_out};
553 my( $self, @evts ) = @_;
556 $e->{payload} = $self->copy if
557 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
559 $logger->info("circulator: pushing event ".$e->{textcode});
560 push( @{$self->events}, $e ) unless
561 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
567 return '' if $self->skip_permit_key;
568 my $key = md5_hex( time() . rand() . "$$" );
569 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
570 return $self->permit_key($key);
573 sub check_permit_key {
575 return 1 if $self->skip_permit_key;
576 my $key = $self->permit_key;
577 return 0 unless $key;
578 my $k = "oils_permit_key_$key";
579 my $one = $self->cache_handle->get_cache($k);
580 $self->cache_handle->delete_cache($k);
581 return ($one) ? 1 : 0;
584 sub seems_like_reservation {
587 # Some words about the following method:
588 # 1) It requires the VIEW_USER permission, but that's not an
589 # issue, right, since all staff should have that?
590 # 2) It returns only one reservation at a time, even if an item can be
591 # and is currently overbooked. Hmmm....
592 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
593 my $result = $booking_ses->request(
594 "open-ils.booking.reservations.by_returnable_resource_barcode",
595 $self->editor->authtoken,
598 $booking_ses->disconnect;
600 return $self->bail_on_events($result) if defined $U->event_code($result);
603 $self->reservation(shift @$result);
611 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
612 sub save_trimmed_copy {
613 my ($self, $copy) = @_;
616 $self->volume($copy->call_number);
617 $self->title($self->volume->record);
618 $self->copy->call_number($self->volume->id);
619 $self->volume->record($self->title->id);
620 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
621 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
622 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
623 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
629 my $e = $self->editor;
631 # --------------------------------------------------------------------------
632 # Grab the fleshed copy
633 # --------------------------------------------------------------------------
634 unless($self->is_noncat) {
637 $copy = $e->retrieve_asset_copy(
638 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
640 } elsif( $self->copy_barcode ) {
642 $copy = $e->search_asset_copy(
643 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
644 } elsif( $self->reservation ) {
645 my $res = $e->json_query(
647 "select" => {"acp" => ["id"]},
652 "field" => "barcode",
656 "field" => "current_resource"
664 "id" => (ref $self->reservation) ?
665 $self->reservation->id : $self->reservation
670 if (ref $res eq "ARRAY" and scalar @$res) {
671 $logger->info("circulator: mapped reservation " .
672 $self->reservation . " to copy " . $res->[0]->{"id"});
673 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
678 $self->save_trimmed_copy($copy);
680 # We can't renew if there is no copy
681 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
682 if $self->is_renewal;
687 # --------------------------------------------------------------------------
689 # --------------------------------------------------------------------------
693 flesh_fields => {au => [ qw/ card / ]}
696 if( $self->patron_id ) {
697 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
698 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
700 } elsif( $self->patron_barcode ) {
702 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
703 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
704 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
706 $patron = $e->retrieve_actor_user($card->usr)
707 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
709 # Use the card we looked up, not the patron's primary, for card active checks
710 $patron->card($card);
713 if( my $copy = $self->copy ) {
716 $flesh->{flesh_fields}->{circ} = ['usr'];
718 my $circ = $e->search_action_circulation([
719 {target_copy => $copy->id, checkin_time => undef}, $flesh
723 $patron = $circ->usr;
724 $circ->usr($patron->id); # de-flesh for consistency
730 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
731 unless $self->patron($patron) or $self->is_checkin;
733 unless($self->is_checkin) {
735 # Check for inactivity and patron reg. expiration
737 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
738 unless $U->is_true($patron->active);
740 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
741 unless $U->is_true($patron->card->active);
743 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
744 cleanse_ISO8601($patron->expire_date));
746 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
747 if( CORE::time > $expire->epoch ) ;
752 # --------------------------------------------------------------------------
753 # Does the circ permit work
754 # --------------------------------------------------------------------------
758 $self->log_me("do_permit()");
760 unless( $self->editor->requestor->id == $self->patron->id ) {
761 return $self->bail_on_events($self->editor->event)
762 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
765 $self->check_captured_holds();
766 $self->do_copy_checks();
767 return if $self->bail_out;
768 $self->run_patron_permit_scripts();
769 $self->run_copy_permit_scripts()
770 unless $self->is_precat or $self->is_noncat;
771 $self->check_item_deposit_events();
772 $self->override_events();
773 return if $self->bail_out;
775 if($self->is_precat and not $self->request_precat) {
778 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
779 return $self->bail_out(1) unless $self->is_renewal;
783 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
786 sub check_item_deposit_events {
788 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
789 if $self->is_deposit and not $self->is_deposit_exempt;
790 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
791 if $self->is_rental and not $self->is_rental_exempt;
794 # returns true if the user is not required to pay deposits
795 sub is_deposit_exempt {
797 my $pid = (ref $self->patron->profile) ?
798 $self->patron->profile->id : $self->patron->profile;
799 my $groups = $U->ou_ancestor_setting_value(
800 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
801 for my $grp (@$groups) {
802 return 1 if $self->is_group_descendant($grp, $pid);
807 # returns true if the user is not required to pay rental fees
808 sub is_rental_exempt {
810 my $pid = (ref $self->patron->profile) ?
811 $self->patron->profile->id : $self->patron->profile;
812 my $groups = $U->ou_ancestor_setting_value(
813 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
814 for my $grp (@$groups) {
815 return 1 if $self->is_group_descendant($grp, $pid);
820 sub is_group_descendant {
821 my($self, $p_id, $c_id) = @_;
822 return 0 unless defined $p_id and defined $c_id;
823 return 1 if $c_id == $p_id;
824 while(my $grp = $user_groups{$c_id}) {
825 $c_id = $grp->parent;
826 return 0 unless defined $c_id;
827 return 1 if $c_id == $p_id;
832 sub check_captured_holds {
834 my $copy = $self->copy;
835 my $patron = $self->patron;
837 return undef unless $copy;
839 my $s = $U->copy_status($copy->status)->id;
840 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
841 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
843 # Item is on the holds shelf, make sure it's going to the right person
844 my $hold = $self->editor->search_action_hold_request(
847 current_copy => $copy->id ,
848 capture_time => { '!=' => undef },
849 cancel_time => undef,
850 fulfillment_time => undef
856 if ($hold and $hold->usr == $patron->id) {
857 $self->checkout_is_for_hold(1);
861 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
863 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
869 my $copy = $self->copy;
872 my $stat = $U->copy_status($copy->status)->id;
874 # We cannot check out a copy if it is in-transit
875 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
876 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
879 $self->handle_claims_returned();
880 return if $self->bail_out;
882 # no claims returned circ was found, check if there is any open circ
883 unless( $self->is_renewal ) {
885 my $circs = $self->editor->search_action_circulation(
886 { target_copy => $copy->id, checkin_time => undef }
889 if(my $old_circ = $circs->[0]) { # an open circ was found
891 my $payload = {copy => $copy};
893 if($old_circ->usr == $self->patron->id) {
895 $payload->{old_circ} = $old_circ;
897 # If there is an open circulation on the checkout item and an auto-renew
898 # interval is defined, inform the caller that they should go
899 # ahead and renew the item instead of warning about open circulations.
901 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
903 'circ.checkout_auto_renew_age',
907 if($auto_renew_intvl) {
908 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
909 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
911 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
912 $payload->{auto_renew} = 1;
917 return $self->bail_on_events(
918 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
924 my $LEGACY_CIRC_EVENT_MAP = {
925 'no_item' => 'ITEM_NOT_CATALOGED',
926 'actor.usr.barred' => 'PATRON_BARRED',
927 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
928 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
929 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
930 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
931 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
932 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
933 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
934 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
935 'config.circ_matrix_test.total_copy_hold_ratio' =>
936 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
937 'config.circ_matrix_test.available_copy_hold_ratio' =>
938 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
942 # ---------------------------------------------------------------------
943 # This pushes any patron-related events into the list but does not
944 # set bail_out for any events
945 # ---------------------------------------------------------------------
946 sub run_patron_permit_scripts {
948 my $patronid = $self->patron->id;
953 my $results = $self->run_indb_circ_test;
954 unless($self->circ_test_success) {
957 if ($self->is_noncat) {
958 # no_item result is OK during noncat checkout
959 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
963 if ($self->checkout_is_for_hold) {
964 # if this checkout will fulfill a hold, ignore CIRC blocks
965 # and rely instead on the (later-checked) FULFILL block
967 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
968 my $fblock_pens = $self->editor->search_config_standing_penalty(
969 {name => [@pen_names], block_list => {like => '%CIRC%'}});
971 for my $res (@$results) {
972 my $name = $res->{fail_part} || '';
973 next if grep {$_->name eq $name} @$fblock_pens;
974 push(@trimmed_results, $res);
978 # not for hold or noncat
979 @trimmed_results = @$results;
983 # update the final set of test results
984 $self->matrix_test_result(\@trimmed_results);
986 push @allevents, $self->matrix_test_result_events;
990 $_->{payload} = $self->copy if
991 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
994 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
996 $self->push_events(@allevents);
999 sub matrix_test_result_codes {
1001 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1004 sub matrix_test_result_events {
1007 my $event = new OpenILS::Event(
1008 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1010 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1012 } (@{$self->matrix_test_result});
1015 sub run_indb_circ_test {
1017 return $self->matrix_test_result if $self->matrix_test_result;
1019 my $dbfunc = ($self->is_renewal) ?
1020 'action.item_user_renew_test' : 'action.item_user_circ_test';
1022 if( $self->is_precat && $self->request_precat) {
1023 $self->make_precat_copy;
1024 return if $self->bail_out;
1027 my $results = $self->editor->json_query(
1031 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1037 $self->circ_test_success($U->is_true($results->[0]->{success}));
1039 if(my $mp = $results->[0]->{matchpoint}) {
1040 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1041 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1042 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1043 if(defined($results->[0]->{renewals})) {
1044 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1046 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1047 if(defined($results->[0]->{grace_period})) {
1048 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1050 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1051 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1052 # Grab the *last* response for limit_groups, where it is more likely to be filled
1053 $self->limit_groups($results->[-1]->{limit_groups});
1056 return $self->matrix_test_result($results);
1059 # ---------------------------------------------------------------------
1060 # given a use and copy, this will calculate the circulation policy
1061 # parameters. Only works with in-db circ.
1062 # ---------------------------------------------------------------------
1066 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1068 $self->run_indb_circ_test;
1071 circ_test_success => $self->circ_test_success,
1072 failure_events => [],
1073 failure_codes => [],
1074 matchpoint => $self->circ_matrix_matchpoint
1077 unless($self->circ_test_success) {
1078 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1079 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1082 if($self->circ_matrix_matchpoint) {
1083 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1084 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1085 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1086 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1088 my $policy = $self->get_circ_policy(
1089 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1091 $$results{$_} = $$policy{$_} for keys %$policy;
1097 # ---------------------------------------------------------------------
1098 # Loads the circ policy info for duration, recurring fine, and max
1099 # fine based on the current copy
1100 # ---------------------------------------------------------------------
1101 sub get_circ_policy {
1102 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1105 duration_rule => $duration_rule->name,
1106 recurring_fine_rule => $recurring_fine_rule->name,
1107 max_fine_rule => $max_fine_rule->name,
1108 max_fine => $self->get_max_fine_amount($max_fine_rule),
1109 fine_interval => $recurring_fine_rule->recurrence_interval,
1110 renewal_remaining => $duration_rule->max_renewals,
1111 grace_period => $recurring_fine_rule->grace_period
1114 if($hard_due_date) {
1115 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1116 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1119 $policy->{duration_date_ceiling} = undef;
1120 $policy->{duration_date_ceiling_force} = undef;
1123 $policy->{duration} = $duration_rule->shrt
1124 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1125 $policy->{duration} = $duration_rule->normal
1126 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1127 $policy->{duration} = $duration_rule->extended
1128 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1130 $policy->{recurring_fine} = $recurring_fine_rule->low
1131 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1132 $policy->{recurring_fine} = $recurring_fine_rule->normal
1133 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1134 $policy->{recurring_fine} = $recurring_fine_rule->high
1135 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1140 sub get_max_fine_amount {
1142 my $max_fine_rule = shift;
1143 my $max_amount = $max_fine_rule->amount;
1145 # if is_percent is true then the max->amount is
1146 # use as a percentage of the copy price
1147 if ($U->is_true($max_fine_rule->is_percent)) {
1148 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1149 $max_amount = $price * $max_fine_rule->amount / 100;
1151 $U->ou_ancestor_setting_value(
1153 'circ.max_fine.cap_at_price',
1157 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1158 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1166 sub run_copy_permit_scripts {
1168 my $copy = $self->copy || return;
1172 my $results = $self->run_indb_circ_test;
1173 push @allevents, $self->matrix_test_result_events
1174 unless $self->circ_test_success;
1176 # See if this copy has an alert message
1177 my $ae = $self->check_copy_alert();
1178 push( @allevents, $ae ) if $ae;
1180 # uniquify the events
1181 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1182 @allevents = values %hash;
1184 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1186 $self->push_events(@allevents);
1190 sub check_copy_alert {
1192 return undef if $self->is_renewal;
1193 return OpenILS::Event->new(
1194 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1195 if $self->copy and $self->copy->alert_message;
1201 # --------------------------------------------------------------------------
1202 # If the call is overriding and has permissions to override every collected
1203 # event, the are cleared. Any event that the caller does not have
1204 # permission to override, will be left in the event list and bail_out will
1206 # XXX We need code in here to cancel any holds/transits on copies
1207 # that are being force-checked out
1208 # --------------------------------------------------------------------------
1209 sub override_events {
1211 my @events = @{$self->events};
1212 return unless @events;
1213 my $oargs = $self->override_args;
1215 if(!$self->override) {
1216 return $self->bail_out(1)
1217 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1222 for my $e (@events) {
1223 my $tc = $e->{textcode};
1224 next if $tc eq 'SUCCESS';
1225 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1226 my $ov = "$tc.override";
1227 $logger->info("circulator: attempting to override event: $ov");
1229 return $self->bail_on_events($self->editor->event)
1230 unless( $self->editor->allowed($ov) );
1232 return $self->bail_out(1);
1238 # --------------------------------------------------------------------------
1239 # If there is an open claimsreturn circ on the requested copy, close the
1240 # circ if overriding, otherwise bail out
1241 # --------------------------------------------------------------------------
1242 sub handle_claims_returned {
1244 my $copy = $self->copy;
1246 my $CR = $self->editor->search_action_circulation(
1248 target_copy => $copy->id,
1249 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1250 checkin_time => undef,
1254 return unless ($CR = $CR->[0]);
1258 # - If the caller has set the override flag, we will check the item in
1259 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1261 $CR->checkin_time('now');
1262 $CR->checkin_scan_time('now');
1263 $CR->checkin_lib($self->circ_lib);
1264 $CR->checkin_workstation($self->editor->requestor->wsid);
1265 $CR->checkin_staff($self->editor->requestor->id);
1267 $evt = $self->editor->event
1268 unless $self->editor->update_action_circulation($CR);
1271 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1274 $self->bail_on_events($evt) if $evt;
1279 # --------------------------------------------------------------------------
1280 # This performs the checkout
1281 # --------------------------------------------------------------------------
1285 $self->log_me("do_checkout()");
1287 # make sure perms are good if this isn't a renewal
1288 unless( $self->is_renewal ) {
1289 return $self->bail_on_events($self->editor->event)
1290 unless( $self->editor->allowed('COPY_CHECKOUT') );
1293 # verify the permit key
1294 unless( $self->check_permit_key ) {
1295 if( $self->permit_override ) {
1296 return $self->bail_on_events($self->editor->event)
1297 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1299 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1303 # if this is a non-cataloged circ, build the circ and finish
1304 if( $self->is_noncat ) {
1305 $self->checkout_noncat;
1307 OpenILS::Event->new('SUCCESS',
1308 payload => { noncat_circ => $self->circ }));
1312 if( $self->is_precat ) {
1313 $self->make_precat_copy;
1314 return if $self->bail_out;
1316 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1317 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1320 $self->do_copy_checks;
1321 return if $self->bail_out;
1323 $self->run_checkout_scripts();
1324 return if $self->bail_out;
1326 $self->build_checkout_circ_object();
1327 return if $self->bail_out;
1329 my $modify_to_start = $self->booking_adjusted_due_date();
1330 return if $self->bail_out;
1332 $self->apply_modified_due_date($modify_to_start);
1333 return if $self->bail_out;
1335 return $self->bail_on_events($self->editor->event)
1336 unless $self->editor->create_action_circulation($self->circ);
1338 # refresh the circ to force local time zone for now
1339 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1341 if($self->limit_groups) {
1342 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1345 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1347 return if $self->bail_out;
1349 $self->apply_deposit_fee();
1350 return if $self->bail_out;
1352 $self->handle_checkout_holds();
1353 return if $self->bail_out;
1355 # ------------------------------------------------------------------------------
1356 # Update the patron penalty info in the DB. Run it for permit-overrides
1357 # since the penalties are not updated during the permit phase
1358 # ------------------------------------------------------------------------------
1359 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1361 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1364 if($self->is_renewal) {
1365 # flesh the billing summary for the checked-in circ
1366 $pcirc = $self->editor->retrieve_action_circulation([
1368 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1373 OpenILS::Event->new('SUCCESS',
1375 copy => $U->unflesh_copy($self->copy),
1376 volume => $self->volume,
1377 circ => $self->circ,
1379 holds_fulfilled => $self->fulfilled_holds,
1380 deposit_billing => $self->deposit_billing,
1381 rental_billing => $self->rental_billing,
1382 parent_circ => $pcirc,
1383 patron => ($self->return_patron) ? $self->patron : undef,
1384 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1390 sub apply_deposit_fee {
1392 my $copy = $self->copy;
1394 ($self->is_deposit and not $self->is_deposit_exempt) or
1395 ($self->is_rental and not $self->is_rental_exempt);
1397 return if $self->is_deposit and $self->skip_deposit_fee;
1398 return if $self->is_rental and $self->skip_rental_fee;
1400 my $bill = Fieldmapper::money::billing->new;
1401 my $amount = $copy->deposit_amount;
1405 if($self->is_deposit) {
1406 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1408 $self->deposit_billing($bill);
1410 $billing_type = OILS_BILLING_TYPE_RENTAL;
1412 $self->rental_billing($bill);
1415 $bill->xact($self->circ->id);
1416 $bill->amount($amount);
1417 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1418 $bill->billing_type($billing_type);
1419 $bill->btype($btype);
1420 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1422 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1427 my $copy = $self->copy;
1429 my $stat = $copy->status if ref $copy->status;
1430 my $loc = $copy->location if ref $copy->location;
1431 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1433 $copy->status($stat->id) if $stat;
1434 $copy->location($loc->id) if $loc;
1435 $copy->circ_lib($circ_lib->id) if $circ_lib;
1436 $copy->editor($self->editor->requestor->id);
1437 $copy->edit_date('now');
1438 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1440 return $self->bail_on_events($self->editor->event)
1441 unless $self->editor->update_asset_copy($self->copy);
1443 $copy->status($U->copy_status($copy->status));
1444 $copy->location($loc) if $loc;
1445 $copy->circ_lib($circ_lib) if $circ_lib;
1448 sub update_reservation {
1450 my $reservation = $self->reservation;
1452 my $usr = $reservation->usr;
1453 my $target_rt = $reservation->target_resource_type;
1454 my $target_r = $reservation->target_resource;
1455 my $current_r = $reservation->current_resource;
1457 $reservation->usr($usr->id) if ref $usr;
1458 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1459 $reservation->target_resource($target_r->id) if ref $target_r;
1460 $reservation->current_resource($current_r->id) if ref $current_r;
1462 return $self->bail_on_events($self->editor->event)
1463 unless $self->editor->update_booking_reservation($self->reservation);
1466 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1467 $self->reservation($reservation);
1471 sub bail_on_events {
1472 my( $self, @evts ) = @_;
1473 $self->push_events(@evts);
1477 # ------------------------------------------------------------------------------
1478 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1479 # affects copies that will fulfill holds and CIRC affects all other copies.
1480 # If blocks exists, bail, push Events onto the event pile, and return true.
1481 # ------------------------------------------------------------------------------
1482 sub check_hold_fulfill_blocks {
1485 # With the addition of ignore_proximity in csp, we need to fetch
1486 # the proximity of both the circ_lib and the copy's circ_lib to
1487 # the patron's home_ou.
1488 my ($ou_prox, $copy_prox);
1489 my $home_ou = (ref($self->patron->home_ou)) ? $self->patron->home_ou->id : $self->patron->home_ou;
1490 $ou_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $self->circ_lib);
1491 $ou_prox = -1 unless (defined($ou_prox));
1492 my $copy_ou = (ref($self->copy->circ_lib)) ? $self->copy->circ_lib->id : $self->copy->circ_lib;
1493 if ($copy_ou == $self->circ_lib) {
1494 # Save us the time of an extra query.
1495 $copy_prox = $ou_prox;
1497 $copy_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $copy_ou);
1498 $copy_prox = -1 unless (defined($copy_prox));
1501 # See if the user has any penalties applied that prevent hold fulfillment
1502 my $pens = $self->editor->json_query({
1503 select => {csp => ['name', 'label']},
1504 from => {ausp => {csp => {}}},
1507 usr => $self->patron->id,
1508 org_unit => $U->get_org_full_path($self->circ_lib),
1510 {stop_date => undef},
1511 {stop_date => {'>' => 'now'}}
1515 block_list => {'like' => '%FULFILL%'},
1517 {ignore_proximity => undef},
1518 {ignore_proximity => {'<' => $ou_prox}},
1519 {ignore_proximity => {'<' => $copy_prox}}
1525 return 0 unless @$pens;
1527 for my $pen (@$pens) {
1528 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1529 my $event = OpenILS::Event->new($pen->{name});
1530 $event->{desc} = $pen->{label};
1531 $self->push_events($event);
1534 $self->override_events;
1535 return $self->bail_out;
1539 # ------------------------------------------------------------------------------
1540 # When an item is checked out, see if we can fulfill a hold for this patron
1541 # ------------------------------------------------------------------------------
1542 sub handle_checkout_holds {
1544 my $copy = $self->copy;
1545 my $patron = $self->patron;
1547 my $e = $self->editor;
1548 $self->fulfilled_holds([]);
1550 # non-cats can't fulfill a hold
1551 return if $self->is_noncat;
1553 my $hold = $e->search_action_hold_request({
1554 current_copy => $copy->id ,
1555 cancel_time => undef,
1556 fulfillment_time => undef,
1558 {expire_time => undef},
1559 {expire_time => {'>' => 'now'}}
1563 if($hold and $hold->usr != $patron->id) {
1564 # reset the hold since the copy is now checked out
1566 $logger->info("circulator: un-targeting hold ".$hold->id.
1567 " because copy ".$copy->id." is getting checked out");
1569 $hold->clear_prev_check_time;
1570 $hold->clear_current_copy;
1571 $hold->clear_capture_time;
1572 $hold->clear_shelf_time;
1573 $hold->clear_shelf_expire_time;
1574 $hold->clear_current_shelf_lib;
1576 return $self->bail_on_event($e->event)
1577 unless $e->update_action_hold_request($hold);
1583 $hold = $self->find_related_user_hold($copy, $patron) or return;
1584 $logger->info("circulator: found related hold to fulfill in checkout");
1587 return if $self->check_hold_fulfill_blocks;
1589 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1591 # if the hold was never officially captured, capture it.
1592 $hold->current_copy($copy->id);
1593 $hold->capture_time('now') unless $hold->capture_time;
1594 $hold->fulfillment_time('now');
1595 $hold->fulfillment_staff($e->requestor->id);
1596 $hold->fulfillment_lib($self->circ_lib);
1598 return $self->bail_on_events($e->event)
1599 unless $e->update_action_hold_request($hold);
1601 return $self->fulfilled_holds([$hold->id]);
1605 # ------------------------------------------------------------------------------
1606 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1607 # the patron directly targets the checked out item, see if there is another hold
1608 # for the patron that could be fulfilled by the checked out item. Fulfill the
1609 # oldest hold and only fulfill 1 of them.
1611 # For "another hold":
1613 # First, check for one that the copy matches via hold_copy_map, ensuring that
1614 # *any* hold type that this copy could fill may end up filled.
1616 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1617 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1618 # that are non-requestable to count as capturing those hold types.
1619 # ------------------------------------------------------------------------------
1620 sub find_related_user_hold {
1621 my($self, $copy, $patron) = @_;
1622 my $e = $self->editor;
1624 # holds on precat copies are always copy-level, so this call will
1625 # always return undef. Exit early.
1626 return undef if $self->is_precat;
1628 return undef unless $U->ou_ancestor_setting_value(
1629 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1631 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1633 select => {ahr => ['id']},
1642 fkey => 'current_copy',
1643 type => 'left' # there may be no current_copy
1650 fulfillment_time => undef,
1651 cancel_time => undef,
1653 {expire_time => undef},
1654 {expire_time => {'>' => 'now'}}
1658 target_copy => $self->copy->id
1662 {id => undef}, # left-join copy may be nonexistent
1663 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1667 order_by => {ahr => {request_time => {direction => 'asc'}}},
1671 my $hold_info = $e->json_query($args)->[0];
1672 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1673 return undef if $U->ou_ancestor_setting_value(
1674 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1676 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1678 select => {ahr => ['id']},
1683 fkey => 'current_copy',
1684 type => 'left' # there may be no current_copy
1691 fulfillment_time => undef,
1692 cancel_time => undef,
1694 {expire_time => undef},
1695 {expire_time => {'>' => 'now'}}
1702 target => $self->volume->id
1708 target => $self->title->id
1714 {id => undef}, # left-join copy may be nonexistent
1715 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1719 order_by => {ahr => {request_time => {direction => 'asc'}}},
1723 $hold_info = $e->json_query($args)->[0];
1724 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1729 sub run_checkout_scripts {
1742 my $hard_due_date_name;
1744 $self->run_indb_circ_test();
1745 $duration = $self->circ_matrix_matchpoint->duration_rule;
1746 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1747 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1748 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1750 $duration_name = $duration->name if $duration;
1751 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1754 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1755 return $self->bail_on_events($evt) if ($evt && !$nobail);
1757 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1758 return $self->bail_on_events($evt) if ($evt && !$nobail);
1760 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1761 return $self->bail_on_events($evt) if ($evt && !$nobail);
1763 if($hard_due_date_name) {
1764 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1765 return $self->bail_on_events($evt) if ($evt && !$nobail);
1771 # The item circulates with an unlimited duration
1775 $hard_due_date = undef;
1778 $self->duration_rule($duration);
1779 $self->recurring_fines_rule($recurring);
1780 $self->max_fine_rule($max_fine);
1781 $self->hard_due_date($hard_due_date);
1785 sub build_checkout_circ_object {
1788 my $circ = Fieldmapper::action::circulation->new;
1789 my $duration = $self->duration_rule;
1790 my $max = $self->max_fine_rule;
1791 my $recurring = $self->recurring_fines_rule;
1792 my $hard_due_date = $self->hard_due_date;
1793 my $copy = $self->copy;
1794 my $patron = $self->patron;
1795 my $duration_date_ceiling;
1796 my $duration_date_ceiling_force;
1800 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1801 $duration_date_ceiling = $policy->{duration_date_ceiling};
1802 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1804 my $dname = $duration->name;
1805 my $mname = $max->name;
1806 my $rname = $recurring->name;
1808 if($hard_due_date) {
1809 $hdname = $hard_due_date->name;
1812 $logger->debug("circulator: building circulation ".
1813 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1815 $circ->duration($policy->{duration});
1816 $circ->recurring_fine($policy->{recurring_fine});
1817 $circ->duration_rule($duration->name);
1818 $circ->recurring_fine_rule($recurring->name);
1819 $circ->max_fine_rule($max->name);
1820 $circ->max_fine($policy->{max_fine});
1821 $circ->fine_interval($recurring->recurrence_interval);
1822 $circ->renewal_remaining($duration->max_renewals);
1823 $circ->grace_period($policy->{grace_period});
1827 $logger->info("circulator: copy found with an unlimited circ duration");
1828 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1829 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1830 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1831 $circ->renewal_remaining(0);
1832 $circ->grace_period(0);
1835 $circ->target_copy( $copy->id );
1836 $circ->usr( $patron->id );
1837 $circ->circ_lib( $self->circ_lib );
1838 $circ->workstation($self->editor->requestor->wsid)
1839 if defined $self->editor->requestor->wsid;
1841 # renewals maintain a link to the parent circulation
1842 $circ->parent_circ($self->parent_circ);
1844 if( $self->is_renewal ) {
1845 $circ->opac_renewal('t') if $self->opac_renewal;
1846 $circ->phone_renewal('t') if $self->phone_renewal;
1847 $circ->desk_renewal('t') if $self->desk_renewal;
1848 $circ->renewal_remaining($self->renewal_remaining);
1849 $circ->circ_staff($self->editor->requestor->id);
1853 # if the user provided an overiding checkout time,
1854 # (e.g. the checkout really happened several hours ago), then
1855 # we apply that here. Does this need a perm??
1856 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1857 if $self->checkout_time;
1859 # if a patron is renewing, 'requestor' will be the patron
1860 $circ->circ_staff($self->editor->requestor->id);
1861 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
1866 sub do_reservation_pickup {
1869 $self->log_me("do_reservation_pickup()");
1871 $self->reservation->pickup_time('now');
1874 $self->reservation->current_resource &&
1875 $U->is_true($self->reservation->target_resource_type->catalog_item)
1877 # We used to try to set $self->copy and $self->patron here,
1878 # but that should already be done.
1880 $self->run_checkout_scripts(1);
1882 my $duration = $self->duration_rule;
1883 my $max = $self->max_fine_rule;
1884 my $recurring = $self->recurring_fines_rule;
1886 if ($duration && $max && $recurring) {
1887 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1889 my $dname = $duration->name;
1890 my $mname = $max->name;
1891 my $rname = $recurring->name;
1893 $logger->debug("circulator: updating reservation ".
1894 "with duration=$dname, maxfine=$mname, recurring=$rname");
1896 $self->reservation->fine_amount($policy->{recurring_fine});
1897 $self->reservation->max_fine($policy->{max_fine});
1898 $self->reservation->fine_interval($recurring->recurrence_interval);
1901 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1902 $self->update_copy();
1905 $self->reservation->fine_amount(
1906 $self->reservation->target_resource_type->fine_amount
1908 $self->reservation->max_fine(
1909 $self->reservation->target_resource_type->max_fine
1911 $self->reservation->fine_interval(
1912 $self->reservation->target_resource_type->fine_interval
1916 $self->update_reservation();
1919 sub do_reservation_return {
1921 my $request = shift;
1923 $self->log_me("do_reservation_return()");
1925 if (not ref $self->reservation) {
1926 my ($reservation, $evt) =
1927 $U->fetch_booking_reservation($self->reservation);
1928 return $self->bail_on_events($evt) if $evt;
1929 $self->reservation($reservation);
1932 $self->handle_fines(1);
1933 $self->reservation->return_time('now');
1934 $self->update_reservation();
1935 $self->reshelve_copy if $self->copy;
1937 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1938 $self->copy( $self->reservation->current_resource->catalog_item );
1942 sub booking_adjusted_due_date {
1944 my $circ = $self->circ;
1945 my $copy = $self->copy;
1947 return undef unless $self->use_booking;
1951 if( $self->due_date ) {
1953 return $self->bail_on_events($self->editor->event)
1954 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1956 $circ->due_date(cleanse_ISO8601($self->due_date));
1960 return unless $copy and $circ->due_date;
1963 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1964 if (@$booking_items) {
1965 my $booking_item = $booking_items->[0];
1966 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1968 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1969 my $shorten_circ_setting = $resource_type->elbow_room ||
1970 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1973 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1974 my $bookings = $booking_ses->request('open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken, {
1975 resource => $booking_item->id
1976 , search_start => 'now'
1977 , search_end => $circ->due_date
1978 , fields => { cancel_time => undef, return_time => undef }
1980 $booking_ses->disconnect;
1982 throw OpenSRF::EX::ERROR ("Improper input arguments") unless defined $bookings;
1983 return $self->bail_on_events($bookings) if ref($bookings) eq 'HASH';
1985 my $dt_parser = DateTime::Format::ISO8601->new;
1986 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1988 for my $bid (@$bookings) {
1990 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1992 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1993 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1995 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1996 if ($booking_start < DateTime->now);
1999 if ($U->is_true($stop_circ_setting)) {
2000 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2002 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2003 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2006 # We set the circ duration here only to affect the logic that will
2007 # later (in a DB trigger) mangle the time part of the due date to
2008 # 11:59pm. Having any circ duration that is not a whole number of
2009 # days is enough to prevent the "correction."
2010 my $new_circ_duration = $due_date->epoch - time;
2011 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2012 $circ->duration("$new_circ_duration seconds");
2014 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2018 return $self->bail_on_events($self->editor->event)
2019 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2025 sub apply_modified_due_date {
2027 my $shift_earlier = shift;
2028 my $circ = $self->circ;
2029 my $copy = $self->copy;
2031 if( $self->due_date ) {
2033 return $self->bail_on_events($self->editor->event)
2034 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2036 $circ->due_date(cleanse_ISO8601($self->due_date));
2040 # if the due_date lands on a day when the location is closed
2041 return unless $copy and $circ->due_date;
2043 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2045 # due-date overlap should be determined by the location the item
2046 # is checked out from, not the owning or circ lib of the item
2047 my $org = $self->circ_lib;
2049 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2050 " with an item due date of ".$circ->due_date );
2052 my $dateinfo = $U->storagereq(
2053 'open-ils.storage.actor.org_unit.closed_date.overlap',
2054 $org, $circ->due_date );
2057 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2058 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2060 # XXX make the behavior more dynamic
2061 # for now, we just push the due date to after the close date
2062 if ($shift_earlier) {
2063 $circ->due_date($dateinfo->{start});
2065 $circ->due_date($dateinfo->{end});
2073 sub create_due_date {
2074 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2076 # if there is a raw time component (e.g. from postgres),
2077 # turn it into an interval that interval_to_seconds can parse
2078 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2080 # for now, use the server timezone. TODO: use workstation org timezone
2081 my $due_date = DateTime->now(time_zone => 'local');
2082 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2084 # add the circ duration
2085 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2088 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2089 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2090 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2095 # return ISO8601 time with timezone
2096 return $due_date->strftime('%FT%T%z');
2101 sub make_precat_copy {
2103 my $copy = $self->copy;
2106 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2108 $copy->editor($self->editor->requestor->id);
2109 $copy->edit_date('now');
2110 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2111 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2112 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2113 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2114 $self->update_copy();
2118 $logger->info("circulator: Creating a new precataloged ".
2119 "copy in checkout with barcode " . $self->copy_barcode);
2121 $copy = Fieldmapper::asset::copy->new;
2122 $copy->circ_lib($self->circ_lib);
2123 $copy->creator($self->editor->requestor->id);
2124 $copy->editor($self->editor->requestor->id);
2125 $copy->barcode($self->copy_barcode);
2126 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2127 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2128 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2130 $copy->dummy_title($self->dummy_title || "");
2131 $copy->dummy_author($self->dummy_author || "");
2132 $copy->dummy_isbn($self->dummy_isbn || "");
2133 $copy->circ_modifier($self->circ_modifier);
2136 # See if we need to override the circ_lib for the copy with a configured circ_lib
2137 # Setting is shortname of the org unit
2138 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2139 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2141 if($precat_circ_lib) {
2142 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2145 $self->bail_on_events($self->editor->event);
2149 $copy->circ_lib($org->id);
2153 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2155 $self->push_events($self->editor->event);
2161 sub checkout_noncat {
2167 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2168 my $count = $self->noncat_count || 1;
2169 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2171 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2175 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2176 $self->editor->requestor->id,
2184 $self->push_events($evt);
2192 # If a copy goes into transit and is then checked in before the transit checkin
2193 # interval has expired, push an event onto the overridable events list.
2194 sub check_transit_checkin_interval {
2197 # only concerned with in-transit items
2198 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2200 # no interval, no problem
2201 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2202 return unless $interval;
2204 # capture the transit so we don't have to fetch it again later during checkin
2206 $self->editor->search_action_transit_copy(
2207 {target_copy => $self->copy->id, dest_recv_time => undef}
2211 # transit from X to X for whatever reason has no min interval
2212 return if $self->transit->source == $self->transit->dest;
2214 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2215 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2216 my $horizon = $t_start->add(seconds => $seconds);
2218 # See if we are still within the transit checkin forbidden range
2219 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2220 if $horizon > DateTime->now;
2223 # Retarget local holds at checkin
2224 sub checkin_retarget {
2226 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2227 return unless $self->is_checkin; # Renewals need not be checked
2228 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2229 return if $self->is_precat; # No holds for precats
2230 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2231 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2232 my $status = $U->copy_status($self->copy->status);
2233 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2234 # Specifically target items that are likely new (by status ID)
2235 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2236 my $location = $self->copy->location;
2237 if(!ref($location)) {
2238 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2239 $self->copy->location($location);
2241 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2243 # Fetch holds for the bib
2244 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2245 $self->editor->authtoken,
2248 capture_time => undef, # No touching captured holds
2249 frozen => 'f', # Don't bother with frozen holds
2250 pickup_lib => $self->circ_lib # Only holds actually here
2253 # Error? Skip the step.
2254 return if exists $result->{"ilsevent"};
2258 foreach my $holdlist (keys %{$result}) {
2259 push @$holds, @{$result->{$holdlist}};
2262 return if scalar(@$holds) == 0; # No holds, no retargeting
2264 # Check for parts on this copy
2265 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2266 my %parts_hash = ();
2267 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2269 # Loop over holds in request-ish order
2270 # Stage 1: Get them into request-ish order
2271 # Also grab type and target for skipping low hanging ones
2272 $result = $self->editor->json_query({
2273 "select" => { "ahr" => ["id", "hold_type", "target"] },
2274 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2275 "where" => { "id" => $holds },
2277 { "class" => "pgt", "field" => "hold_priority"},
2278 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2279 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2280 { "class" => "ahr", "field" => "request_time"}
2285 if (ref $result eq "ARRAY" and scalar @$result) {
2286 foreach (@{$result}) {
2287 # Copy level, but not this copy?
2288 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2289 and $_->{target} != $self->copy->id);
2290 # Volume level, but not this volume?
2291 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2292 if(@$parts) { # We have parts?
2294 next if ($_->{hold_type} eq 'T');
2295 # Skip part holds for parts not on this copy
2296 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2298 # No parts, no part holds
2299 next if ($_->{hold_type} eq 'P');
2301 # So much for easy stuff, attempt a retarget!
2302 my $tresult = $U->simplereq(
2303 'open-ils.hold-targeter',
2304 'open-ils.hold-targeter.target',
2305 {hold => $_->{id}, find_copy => $self->copy->id}
2307 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2308 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2316 $self->log_me("do_checkin()");
2318 return $self->bail_on_events(
2319 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2322 $self->check_transit_checkin_interval;
2323 $self->checkin_retarget;
2325 # the renew code and mk_env should have already found our circulation object
2326 unless( $self->circ ) {
2328 my $circs = $self->editor->search_action_circulation(
2329 { target_copy => $self->copy->id, checkin_time => undef });
2331 $self->circ($$circs[0]);
2333 # for now, just warn if there are multiple open circs on a copy
2334 $logger->warn("circulator: we have ".scalar(@$circs).
2335 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2338 my $stat = $U->copy_status($self->copy->status)->id;
2340 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2341 # differently if they are already paid for. We need to check for this
2342 # early since overdue generation is potentially affected.
2343 my $dont_change_lost_zero = 0;
2344 if ($stat == OILS_COPY_STATUS_LOST
2345 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2346 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2348 # LOST fine settings are controlled by the copy's circ lib, not the the
2350 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2351 $self->copy->circ_lib->id : $self->copy->circ_lib;
2352 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2353 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2354 $self->editor) || 0;
2356 if ($dont_change_lost_zero) {
2357 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2358 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2361 $self->dont_change_lost_zero($dont_change_lost_zero);
2364 if( $self->checkin_check_holds_shelf() ) {
2365 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2366 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2367 if($self->fake_hold_dest) {
2368 $self->hold->pickup_lib($self->circ_lib);
2370 $self->checkin_flesh_events;
2374 unless( $self->is_renewal ) {
2375 return $self->bail_on_events($self->editor->event)
2376 unless $self->editor->allowed('COPY_CHECKIN');
2379 $self->push_events($self->check_copy_alert());
2380 $self->push_events($self->check_checkin_copy_status());
2382 # if the circ is marked as 'claims returned', add the event to the list
2383 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2384 if ($self->circ and $self->circ->stop_fines
2385 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2387 $self->check_circ_deposit();
2389 # handle the overridable events
2390 $self->override_events unless $self->is_renewal;
2391 return if $self->bail_out;
2393 if( $self->copy and !$self->transit ) {
2395 $self->editor->search_action_transit_copy(
2396 { target_copy => $self->copy->id, dest_recv_time => undef }
2402 $self->checkin_handle_circ_start;
2403 return if $self->bail_out;
2405 if (!$dont_change_lost_zero) {
2406 # if this circ is LOST and we are configured to generate overdue
2407 # fines for lost items on checkin (to fill the gap between mark
2408 # lost time and when the fines would have naturally stopped), then
2409 # stop_fines is no longer valid and should be cleared.
2411 # stop_fines will be set again during the handle_fines() stage.
2412 # XXX should this setting come from the copy circ lib (like other
2413 # LOST settings), instead of the circulation circ lib?
2414 if ($stat == OILS_COPY_STATUS_LOST) {
2415 $self->circ->clear_stop_fines if
2416 $U->ou_ancestor_setting_value(
2418 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2423 # Set stop_fines when claimed never checked out
2424 $self->circ->stop_fines( OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT ) if( $self->claims_never_checked_out );
2426 # handle fines for this circ, including overdue gen if needed
2427 $self->handle_fines;
2430 $self->checkin_handle_circ_finish;
2431 return if $self->bail_out;
2432 $self->checkin_changed(1);
2434 } elsif( $self->transit ) {
2435 my $hold_transit = $self->process_received_transit;
2436 $self->checkin_changed(1);
2438 if( $self->bail_out ) {
2439 $self->checkin_flesh_events;
2443 if( my $e = $self->check_checkin_copy_status() ) {
2444 # If the original copy status is special, alert the caller
2445 my $ev = $self->events;
2446 $self->events([$e]);
2447 $self->override_events;
2448 return if $self->bail_out;
2452 if( $hold_transit or
2453 $U->copy_status($self->copy->status)->id
2454 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2457 if( $hold_transit ) {
2458 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2460 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2465 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2467 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2468 $self->reshelve_copy(1);
2469 $self->cancelled_hold_transit(1);
2470 $self->notify_hold(0); # don't notify for cancelled holds
2471 $self->fake_hold_dest(0);
2472 return if $self->bail_out;
2474 } elsif ($hold and $hold->hold_type eq 'R') {
2476 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2477 $self->notify_hold(0); # No need to notify
2478 $self->fake_hold_dest(0);
2479 $self->noop(1); # Don't try and capture for other holds/transits now
2480 $self->update_copy();
2481 $hold->fulfillment_time('now');
2482 $self->bail_on_events($self->editor->event)
2483 unless $self->editor->update_action_hold_request($hold);
2487 # hold transited to correct location
2488 if($self->fake_hold_dest) {
2489 $hold->pickup_lib($self->circ_lib);
2491 $self->checkin_flesh_events;
2496 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2498 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2499 " that is in-transit, but there is no transit.. repairing");
2500 $self->reshelve_copy(1);
2501 return if $self->bail_out;
2504 if( $self->is_renewal ) {
2505 $self->finish_fines_and_voiding;
2506 return if $self->bail_out;
2507 $self->push_events(OpenILS::Event->new('SUCCESS'));
2511 # ------------------------------------------------------------------------------
2512 # Circulations and transits are now closed where necessary. Now go on to see if
2513 # this copy can fulfill a hold or needs to be routed to a different location
2514 # ------------------------------------------------------------------------------
2516 my $needed_for_something = 0; # formerly "needed_for_hold"
2518 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2520 if (!$self->remote_hold) {
2521 if ($self->use_booking) {
2522 my $potential_hold = $self->hold_capture_is_possible;
2523 my $potential_reservation = $self->reservation_capture_is_possible;
2525 if ($potential_hold and $potential_reservation) {
2526 $logger->info("circulator: item could fulfill either hold or reservation");
2527 $self->push_events(new OpenILS::Event(
2528 "HOLD_RESERVATION_CONFLICT",
2529 "hold" => $potential_hold,
2530 "reservation" => $potential_reservation
2532 return if $self->bail_out;
2533 } elsif ($potential_hold) {
2534 $needed_for_something =
2535 $self->attempt_checkin_hold_capture;
2536 } elsif ($potential_reservation) {
2537 $needed_for_something =
2538 $self->attempt_checkin_reservation_capture;
2541 $needed_for_something = $self->attempt_checkin_hold_capture;
2544 return if $self->bail_out;
2546 unless($needed_for_something) {
2547 my $circ_lib = (ref $self->copy->circ_lib) ?
2548 $self->copy->circ_lib->id : $self->copy->circ_lib;
2550 if( $self->remote_hold ) {
2551 $circ_lib = $self->remote_hold->pickup_lib;
2552 $logger->warn("circulator: Copy ".$self->copy->barcode.
2553 " is on a remote hold's shelf, sending to $circ_lib");
2556 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2558 my $suppress_transit = 0;
2560 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2561 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2562 if($suppress_transit_source && $suppress_transit_source->{value}) {
2563 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2564 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2565 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2566 $suppress_transit = 1;
2571 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2572 # copy is where it needs to be, either for hold or reshelving
2574 $self->checkin_handle_precat();
2575 return if $self->bail_out;
2578 # copy needs to transit "home", or stick here if it's a floating copy
2580 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2581 my $res = $self->editor->json_query(
2583 'evergreen.can_float',
2584 $self->copy->floating->id,
2585 $self->copy->circ_lib,
2590 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2592 if ($can_float) { # Yep, floating, stick here
2593 $self->checkin_changed(1);
2594 $self->copy->circ_lib( $self->circ_lib );
2597 my $bc = $self->copy->barcode;
2598 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2599 $self->checkin_build_copy_transit($circ_lib);
2600 return if $self->bail_out;
2601 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2605 } else { # no-op checkin
2606 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2607 $self->checkin_changed(1);
2608 $self->copy->circ_lib( $self->circ_lib );
2613 if($self->claims_never_checked_out and
2614 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2616 # the item was not supposed to be checked out to the user and should now be marked as missing
2617 $self->copy->status(OILS_COPY_STATUS_MISSING);
2621 $self->reshelve_copy unless $needed_for_something;
2624 return if $self->bail_out;
2626 unless($self->checkin_changed) {
2628 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2629 my $stat = $U->copy_status($self->copy->status)->id;
2631 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2632 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2633 $self->bail_out(1); # no need to commit anything
2637 $self->push_events(OpenILS::Event->new('SUCCESS'))
2638 unless @{$self->events};
2641 $self->finish_fines_and_voiding;
2643 OpenILS::Utils::Penalty->calculate_penalties(
2644 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2646 $self->checkin_flesh_events;
2650 sub finish_fines_and_voiding {
2652 return unless $self->circ;
2654 return unless $self->backdate or $self->void_overdues;
2656 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2657 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2659 my $evt = $CC->void_or_zero_overdues(
2660 $self->editor, $self->circ, {backdate => $self->void_overdues ? undef : $self->backdate, note => $note});
2662 return $self->bail_on_events($evt) if $evt;
2664 # Make sure the circ is open or closed as necessary.
2665 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2666 return $self->bail_on_events($evt) if $evt;
2672 # if a deposit was payed for this item, push the event
2673 sub check_circ_deposit {
2675 return unless $self->circ;
2676 my $deposit = $self->editor->search_money_billing(
2678 xact => $self->circ->id,
2680 }, {idlist => 1})->[0];
2682 $self->push_events(OpenILS::Event->new(
2683 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2688 my $force = $self->force || shift;
2689 my $copy = $self->copy;
2691 my $stat = $U->copy_status($copy->status)->id;
2694 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2695 $stat != OILS_COPY_STATUS_CATALOGING and
2696 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2697 $stat != OILS_COPY_STATUS_RESHELVING )) {
2699 $copy->status( OILS_COPY_STATUS_RESHELVING );
2701 $self->checkin_changed(1);
2706 # Returns true if the item is at the current location
2707 # because it was transited there for a hold and the
2708 # hold has not been fulfilled
2709 sub checkin_check_holds_shelf {
2711 return 0 unless $self->copy;
2714 $U->copy_status($self->copy->status)->id ==
2715 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2717 # Attempt to clear shelf expired holds for this copy
2718 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2719 if($self->clear_expired);
2721 # find the hold that put us on the holds shelf
2722 my $holds = $self->editor->search_action_hold_request(
2724 current_copy => $self->copy->id,
2725 capture_time => { '!=' => undef },
2726 fulfillment_time => undef,
2727 cancel_time => undef,
2732 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2733 $self->reshelve_copy(1);
2737 my $hold = $$holds[0];
2739 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2740 $hold->id. "] for copy ".$self->copy->barcode);
2742 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2743 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2744 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2745 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2746 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2747 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2748 $self->fake_hold_dest(1);
2754 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2755 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2759 $logger->info("circulator: hold is not for here..");
2760 $self->remote_hold($hold);
2765 sub checkin_handle_precat {
2767 my $copy = $self->copy;
2769 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2770 $copy->status(OILS_COPY_STATUS_CATALOGING);
2771 $self->update_copy();
2772 $self->checkin_changed(1);
2773 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2778 sub checkin_build_copy_transit {
2781 my $copy = $self->copy;
2782 my $transit = Fieldmapper::action::transit_copy->new;
2784 # if we are transiting an item to the shelf shelf, it's a hold transit
2785 if (my $hold = $self->remote_hold) {
2786 $transit = Fieldmapper::action::hold_transit_copy->new;
2787 $transit->hold($hold->id);
2789 # the item is going into transit, remove any shelf-iness
2790 if ($hold->current_shelf_lib or $hold->shelf_time) {
2791 $hold->clear_current_shelf_lib;
2792 $hold->clear_shelf_time;
2793 return $self->bail_on_events($self->editor->event)
2794 unless $self->editor->update_action_hold_request($hold);
2798 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2799 $logger->info("circulator: transiting copy to $dest");
2801 $transit->source($self->circ_lib);
2802 $transit->dest($dest);
2803 $transit->target_copy($copy->id);
2804 $transit->source_send_time('now');
2805 $transit->copy_status( $U->copy_status($copy->status)->id );
2807 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2809 if ($self->remote_hold) {
2810 return $self->bail_on_events($self->editor->event)
2811 unless $self->editor->create_action_hold_transit_copy($transit);
2813 return $self->bail_on_events($self->editor->event)
2814 unless $self->editor->create_action_transit_copy($transit);
2817 # ensure the transit is returned to the caller
2818 $self->transit($transit);
2820 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2822 $self->checkin_changed(1);
2826 sub hold_capture_is_possible {
2828 my $copy = $self->copy;
2830 # we've been explicitly told not to capture any holds
2831 return 0 if $self->capture eq 'nocapture';
2833 # See if this copy can fulfill any holds
2834 my $hold = $holdcode->find_nearest_permitted_hold(
2835 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2837 return undef if ref $hold eq "HASH" and
2838 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2842 sub reservation_capture_is_possible {
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 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2850 my $resv = $booking_ses->request(
2851 "open-ils.booking.reservations.could_capture",
2852 $self->editor->authtoken, $copy->barcode
2854 $booking_ses->disconnect;
2855 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2856 $self->push_events($resv);
2862 # returns true if the item was used (or may potentially be used
2863 # in subsequent calls) to capture a hold.
2864 sub attempt_checkin_hold_capture {
2866 my $copy = $self->copy;
2868 # we've been explicitly told not to capture any holds
2869 return 0 if $self->capture eq 'nocapture';
2871 # See if this copy can fulfill any holds
2872 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2873 $self->editor, $copy, $self->editor->requestor );
2876 $logger->debug("circulator: no potential permitted".
2877 "holds found for copy ".$copy->barcode);
2881 if($self->capture ne 'capture') {
2882 # see if this item is in a hold-capture-delay location
2883 my $location = $self->copy->location;
2884 if(!ref($location)) {
2885 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2886 $self->copy->location($location);
2888 if($U->is_true($location->hold_verify)) {
2889 $self->bail_on_events(
2890 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2895 $self->retarget($retarget);
2897 my $suppress_transit = 0;
2898 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2899 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2900 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2901 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2902 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2903 $suppress_transit = 1;
2904 $hold->pickup_lib($self->circ_lib);
2909 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2911 $hold->current_copy($copy->id);
2912 $hold->capture_time('now');
2913 $self->put_hold_on_shelf($hold)
2914 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2916 # prevent DB errors caused by fetching
2917 # holds from storage, and updating through cstore
2918 $hold->clear_fulfillment_time;
2919 $hold->clear_fulfillment_staff;
2920 $hold->clear_fulfillment_lib;
2921 $hold->clear_expire_time;
2922 $hold->clear_cancel_time;
2923 $hold->clear_prev_check_time unless $hold->prev_check_time;
2925 $self->bail_on_events($self->editor->event)
2926 unless $self->editor->update_action_hold_request($hold);
2928 $self->checkin_changed(1);
2930 return 0 if $self->bail_out;
2932 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2934 if ($hold->hold_type eq 'R') {
2935 $copy->status(OILS_COPY_STATUS_CATALOGING);
2936 $hold->fulfillment_time('now');
2937 $self->noop(1); # Block other transit/hold checks
2938 $self->bail_on_events($self->editor->event)
2939 unless $self->editor->update_action_hold_request($hold);
2941 # This hold was captured in the correct location
2942 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2943 $self->push_events(OpenILS::Event->new('SUCCESS'));
2945 #$self->do_hold_notify($hold->id);
2946 $self->notify_hold($hold->id);
2951 # Hold needs to be picked up elsewhere. Build a hold
2952 # transit and route the item.
2953 $self->checkin_build_hold_transit();
2954 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2955 return 0 if $self->bail_out;
2956 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2959 # make sure we save the copy status
2961 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
2965 sub attempt_checkin_reservation_capture {
2967 my $copy = $self->copy;
2969 # we've been explicitly told not to capture any holds
2970 return 0 if $self->capture eq 'nocapture';
2972 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2973 my $evt = $booking_ses->request(
2974 "open-ils.booking.resources.capture_for_reservation",
2975 $self->editor->authtoken,
2977 1 # don't update copy - we probably have it locked
2979 $booking_ses->disconnect;
2981 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2983 "open-ils.booking.resources.capture_for_reservation " .
2984 "didn't return an event!"
2988 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2989 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2991 # not-transferable is an error event we'll pass on the user
2992 $logger->warn("reservation capture attempted against non-transferable item");
2993 $self->push_events($evt);
2995 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2996 # Re-retrieve copy as reservation capture may have changed
2997 # its status and whatnot.
2999 "circulator: booking capture win on copy " . $self->copy->id
3001 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3003 "circulator: changing copy " . $self->copy->id .
3004 "'s status from " . $self->copy->status . " to " .
3007 $self->copy->status($new_copy_status);
3010 $self->reservation($evt->{"payload"}->{"reservation"});
3012 if (exists $evt->{"payload"}->{"transit"}) {
3016 "org" => $evt->{"payload"}->{"transit"}->dest
3020 $self->checkin_changed(1);
3024 # other results are treated as "nothing to capture"
3028 sub do_hold_notify {
3029 my( $self, $holdid ) = @_;
3031 my $e = new_editor(xact => 1);
3032 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3034 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3035 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3037 $logger->info("circulator: running delayed hold notify process");
3039 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3040 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3042 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3043 hold_id => $holdid, requestor => $self->editor->requestor);
3045 $logger->debug("circulator: built hold notifier");
3047 if(!$notifier->event) {
3049 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3051 my $stat = $notifier->send_email_notify;
3052 if( $stat == '1' ) {
3053 $logger->info("circulator: hold notify succeeded for hold $holdid");
3057 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3060 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3064 sub retarget_holds {
3066 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3067 my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
3068 $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
3069 # no reason to wait for the return value
3073 sub checkin_build_hold_transit {
3076 my $copy = $self->copy;
3077 my $hold = $self->hold;
3078 my $trans = Fieldmapper::action::hold_transit_copy->new;
3080 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3082 $trans->hold($hold->id);
3083 $trans->source($self->circ_lib);
3084 $trans->dest($hold->pickup_lib);
3085 $trans->source_send_time("now");
3086 $trans->target_copy($copy->id);
3088 # when the copy gets to its destination, it will recover
3089 # this status - put it onto the holds shelf
3090 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3092 return $self->bail_on_events($self->editor->event)
3093 unless $self->editor->create_action_hold_transit_copy($trans);
3098 sub process_received_transit {
3100 my $copy = $self->copy;
3101 my $copyid = $self->copy->id;
3103 my $status_name = $U->copy_status($copy->status)->name;
3104 $logger->debug("circulator: attempting transit receive on ".
3105 "copy $copyid. Copy status is $status_name");
3107 my $transit = $self->transit;
3109 # Check if we are in a transit suppress range
3110 my $suppress_transit = 0;
3111 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3112 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3113 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3114 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3115 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3116 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3117 $suppress_transit = 1;
3118 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3122 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3123 # - this item is in-transit to a different location
3124 # - Or we are capturing holds as transits, so why create a new transit?
3126 my $tid = $transit->id;
3127 my $loc = $self->circ_lib;
3128 my $dest = $transit->dest;
3130 $logger->info("circulator: Fowarding transit on copy which is destined ".
3131 "for a different location. transit=$tid, copy=$copyid, current ".
3132 "location=$loc, destination location=$dest");
3134 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3136 # grab the associated hold object if available
3137 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3138 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3140 return $self->bail_on_events($evt);
3143 # The transit is received, set the receive time
3144 $transit->dest_recv_time('now');
3145 $self->bail_on_events($self->editor->event)
3146 unless $self->editor->update_action_transit_copy($transit);
3148 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3150 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3151 $copy->status( $transit->copy_status );
3152 $self->update_copy();
3153 return if $self->bail_out;
3157 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3160 # hold has arrived at destination, set shelf time
3161 $self->put_hold_on_shelf($hold);
3162 $self->bail_on_events($self->editor->event)
3163 unless $self->editor->update_action_hold_request($hold);
3164 return if $self->bail_out;
3166 $self->notify_hold($hold_transit->hold);
3169 $hold_transit = undef;
3170 $self->cancelled_hold_transit(1);
3171 $self->reshelve_copy(1);
3172 $self->fake_hold_dest(0);
3177 OpenILS::Event->new(
3180 payload => { transit => $transit, holdtransit => $hold_transit } ));
3182 return $hold_transit;
3186 # ------------------------------------------------------------------
3187 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3188 # ------------------------------------------------------------------
3189 sub put_hold_on_shelf {
3190 my($self, $hold) = @_;
3191 $hold->shelf_time('now');
3192 $hold->current_shelf_lib($self->circ_lib);
3193 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3199 my $reservation = shift;
3200 my $dt_parser = DateTime::Format::ISO8601->new;
3202 my $obj = $reservation ? $self->reservation : $self->circ;
3204 my $lost_bill_opts = $self->lost_bill_options;
3205 my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3206 # first, restore any voided overdues for lost, if needed
3207 if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3208 my $restore_od = $U->ou_ancestor_setting_value(
3209 $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3210 $self->editor) || 0;
3211 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3215 # next, handle normal overdue generation and apply stop_fines
3216 # XXX reservations don't have stop_fines
3217 # TODO revisit booking_reservation re: stop_fines support
3218 if ($reservation or !$obj->stop_fines) {
3221 # This is a crude check for whether we are in a grace period. The code
3222 # in generate_fines() does a more thorough job, so this exists solely
3223 # as a small optimization, and might be better off removed.
3225 # If we have a grace period
3226 if($obj->can('grace_period')) {
3227 # Parse out the due date
3228 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3229 # Add the grace period to the due date
3230 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3231 # Don't generate fines on circs still in grace period
3232 $skip_for_grace = $due_date > DateTime->now;
3234 $CC->generate_fines({circs => [$obj], editor => $self->editor})
3235 unless $skip_for_grace;
3237 if (!$reservation and !$obj->stop_fines) {
3238 $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3239 $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3240 $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3241 $obj->stop_fines_time('now');
3242 $obj->stop_fines_time($self->backdate) if $self->backdate;
3243 $self->editor->update_action_circulation($obj);
3247 # finally, handle voiding of lost item and processing fees
3248 if ($self->needs_lost_bill_handling) {
3249 my $void_cost = $U->ou_ancestor_setting_value(
3250 $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3251 $self->editor) || 0;
3252 my $void_proc_fee = $U->ou_ancestor_setting_value(
3253 $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3254 $self->editor) || 0;
3255 $self->checkin_handle_lost_or_lo_now_found(
3256 $lost_bill_opts->{void_cost_btype},
3257 $lost_bill_opts->{is_longoverdue}) if $void_cost;
3258 $self->checkin_handle_lost_or_lo_now_found(
3259 $lost_bill_opts->{void_fee_btype},
3260 $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3266 sub checkin_handle_circ_start {
3268 my $circ = $self->circ;
3269 my $copy = $self->copy;
3273 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3275 # backdate the circ if necessary
3276 if($self->backdate) {
3277 my $evt = $self->checkin_handle_backdate;
3278 return $self->bail_on_events($evt) if $evt;
3281 # Set the checkin vars since we have the item
3282 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3284 # capture the true scan time for back-dated checkins
3285 $circ->checkin_scan_time('now');
3287 $circ->checkin_staff($self->editor->requestor->id);
3288 $circ->checkin_lib($self->circ_lib);
3289 $circ->checkin_workstation($self->editor->requestor->wsid);
3291 my $circ_lib = (ref $self->copy->circ_lib) ?
3292 $self->copy->circ_lib->id : $self->copy->circ_lib;
3293 my $stat = $U->copy_status($self->copy->status)->id;
3295 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3296 # we will now handle lost fines, but the copy will retain its 'lost'
3297 # status if it needs to transit home unless lost_immediately_available
3300 # if we decide to also delay fine handling until the item arrives home,
3301 # we will need to call lost fine handling code both when checking items
3302 # in and also when receiving transits
3303 $self->checkin_handle_lost($circ_lib);
3304 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3305 # same process as above.
3306 $self->checkin_handle_long_overdue($circ_lib);
3307 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3308 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3310 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3317 sub checkin_handle_circ_finish {
3319 my $circ = $self->circ;
3321 # see if there are any fines owed on this circ. if not, close it
3322 my ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3323 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3325 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3327 return $self->bail_on_events($self->editor->event)
3328 unless $self->editor->update_action_circulation($circ);
3333 # ------------------------------------------------------------------
3334 # See if we need to void billings, etc. for lost checkin
3335 # ------------------------------------------------------------------
3336 sub checkin_handle_lost {
3338 my $circ_lib = shift;
3340 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3341 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3343 $self->lost_bill_options({
3344 circ_lib => $circ_lib,
3345 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3346 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3347 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3348 void_cost_btype => 3,
3352 return $self->checkin_handle_lost_or_longoverdue(
3353 circ_lib => $circ_lib,
3354 max_return => $max_return,
3355 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3356 ous_use_last_activity => undef # not supported for LOST checkin
3360 # ------------------------------------------------------------------
3361 # See if we need to void billings, etc. for long-overdue checkin
3362 # note: not using constants below since they serve little purpose
3363 # for single-use strings that are descriptive in their own right
3364 # and mostly just complicate debugging.
3365 # ------------------------------------------------------------------
3366 sub checkin_handle_long_overdue {
3368 my $circ_lib = shift;
3370 $logger->info("circulator: processing long-overdue checkin...");
3372 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3373 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3375 $self->lost_bill_options({
3376 circ_lib => $circ_lib,
3377 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3378 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3379 is_longoverdue => 1,
3380 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3381 void_cost_btype => 10,
3382 void_fee_btype => 11
3385 return $self->checkin_handle_lost_or_longoverdue(
3386 circ_lib => $circ_lib,
3387 max_return => $max_return,
3388 ous_immediately_available => 'circ.longoverdue_immediately_available',
3389 ous_use_last_activity =>
3390 'circ.longoverdue.use_last_activity_date_on_return'
3394 # last billing activity is last payment time, last billing time, or the
3395 # circ due date. If the relevant "use last activity" org unit setting is
3396 # false/unset, then last billing activity is always the due date.
3397 sub get_circ_last_billing_activity {
3399 my $circ_lib = shift;
3400 my $setting = shift;
3401 my $date = $self->circ->due_date;
3403 return $date unless $setting and
3404 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3406 my $xact = $self->editor->retrieve_money_billable_transaction([
3408 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3411 if ($xact->summary) {
3412 $date = $xact->summary->last_payment_ts ||
3413 $xact->summary->last_billing_ts ||
3414 $self->circ->due_date;
3421 sub checkin_handle_lost_or_longoverdue {
3422 my ($self, %args) = @_;
3424 my $circ = $self->circ;
3425 my $max_return = $args{max_return};
3426 my $circ_lib = $args{circ_lib};
3431 $self->get_circ_last_billing_activity(
3432 $circ_lib, $args{ous_use_last_activity});
3435 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3436 $tm[5] -= 1 if $tm[5] > 0;
3437 my $due = timelocal(int($tm[1]), int($tm[2]),
3438 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3441 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3443 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3444 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3445 "DUE: $due LAST: $last_chance");
3447 $max_return = 0 if $today < $last_chance;
3453 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3454 "return interval. skipping fine/fee voiding, etc.");
3456 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3458 $logger->info("circulator: check-in of lost/lo item having a balance ".
3459 "of zero, skipping fine/fee voiding and reinstatement.");
3461 } else { # within max-return interval or no interval defined
3463 $logger->info("circulator: check-in of lost/lo item is within the ".
3464 "max return interval (or no interval is defined). Proceeding ".
3465 "with fine/fee voiding, etc.");
3467 $self->needs_lost_bill_handling(1);
3470 if ($circ_lib != $self->circ_lib) {
3471 # if the item is not home, check to see if we want to retain the
3472 # lost/longoverdue status at this point in the process
3474 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3475 $args{ous_immediately_available}, $self->editor) || 0;
3477 if ($immediately_available) {
3478 # item status does not need to be retained, so give it a
3479 # reshelving status as if it were a normal checkin
3480 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3483 $logger->info("circulator: leaving lost/longoverdue copy".
3484 " status in place on checkin");
3487 # lost/longoverdue item is home and processed, treat like a normal
3488 # checkin from this point on
3489 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3495 sub checkin_handle_backdate {
3498 # ------------------------------------------------------------------
3499 # clean up the backdate for date comparison
3500 # XXX We are currently taking the due-time from the original due-date,
3501 # not the input. Do we need to do this? This certainly interferes with
3502 # backdating of hourly checkouts, but that is likely a very rare case.
3503 # ------------------------------------------------------------------
3504 my $bd = cleanse_ISO8601($self->backdate);
3505 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3506 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3507 $new_date->set_hour($original_date->hour());
3508 $new_date->set_minute($original_date->minute());
3509 if ($new_date >= DateTime->now) {
3510 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3511 # $self->backdate() autoload handler ignores undef values.
3512 # Clear the backdate manually.
3513 $logger->info("circulator: ignoring future backdate: $new_date");
3514 delete $self->{backdate};
3516 $self->backdate(cleanse_ISO8601($new_date->datetime()));
3523 sub check_checkin_copy_status {
3525 my $copy = $self->copy;
3527 my $status = $U->copy_status($copy->status)->id;
3530 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3531 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3532 $status == OILS_COPY_STATUS_IN_PROCESS ||
3533 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3534 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3535 $status == OILS_COPY_STATUS_CATALOGING ||
3536 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3537 $status == OILS_COPY_STATUS_CANCELED_TRANSIT ||
3538 $status == OILS_COPY_STATUS_RESHELVING );
3540 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3541 if( $status == OILS_COPY_STATUS_LOST );
3543 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3544 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3546 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3547 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3549 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3550 if( $status == OILS_COPY_STATUS_MISSING );
3552 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3557 # --------------------------------------------------------------------------
3558 # On checkin, we need to return as many relevant objects as we can
3559 # --------------------------------------------------------------------------
3560 sub checkin_flesh_events {
3563 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3564 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3565 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3568 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3571 if($self->hold and !$self->hold->cancel_time) {
3572 $hold = $self->hold;
3573 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3577 # update our copy of the circ object and
3578 # flesh the billing summary data
3580 $self->editor->retrieve_action_circulation([
3584 circ => ['billable_transaction'],
3593 # flesh some patron fields before returning
3595 $self->editor->retrieve_actor_user([
3600 au => ['card', 'billing_address', 'mailing_address']
3607 for my $evt (@{$self->events}) {
3610 $payload->{copy} = $U->unflesh_copy($self->copy);
3611 $payload->{volume} = $self->volume;
3612 $payload->{record} = $record,
3613 $payload->{circ} = $self->circ;
3614 $payload->{transit} = $self->transit;
3615 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3616 $payload->{hold} = $hold;
3617 $payload->{patron} = $self->patron;
3618 $payload->{reservation} = $self->reservation
3619 unless (not $self->reservation or $self->reservation->cancel_time);
3621 $evt->{payload} = $payload;
3626 my( $self, $msg ) = @_;
3627 my $bc = ($self->copy) ? $self->copy->barcode :
3630 my $usr = ($self->patron) ? $self->patron->id : "";
3631 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3632 ", recipient=$usr, copy=$bc");
3638 $self->log_me("do_renew()");
3640 # Make sure there is an open circ to renew
3641 my $usrid = $self->patron->id if $self->patron;
3642 my $circ = $self->editor->search_action_circulation({
3643 target_copy => $self->copy->id,
3644 xact_finish => undef,
3645 checkin_time => undef,
3646 ($usrid ? (usr => $usrid) : ())
3649 return $self->bail_on_events($self->editor->event) unless $circ;
3651 # A user is not allowed to renew another user's items without permission
3652 unless( $circ->usr eq $self->editor->requestor->id ) {
3653 return $self->bail_on_events($self->editor->events)
3654 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3657 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3658 if $circ->renewal_remaining < 1;
3660 # -----------------------------------------------------------------
3662 $self->parent_circ($circ->id);
3663 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3666 # Opac renewal - re-use circ library from original circ (unless told not to)
3667 if($self->opac_renewal) {
3668 unless(defined($opac_renewal_use_circ_lib)) {
3669 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3670 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3671 $opac_renewal_use_circ_lib = 1;
3674 $opac_renewal_use_circ_lib = 0;
3677 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3680 # Desk renewal - re-use circ library from original circ (unless told not to)
3681 if($self->desk_renewal) {
3682 unless(defined($desk_renewal_use_circ_lib)) {
3683 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3684 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3685 $desk_renewal_use_circ_lib = 1;
3688 $desk_renewal_use_circ_lib = 0;
3691 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3694 # Run the fine generator against the old circ
3695 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3696 # a few lines down. Commenting out, for now.
3697 #$self->handle_fines;
3699 $self->run_renew_permit;
3702 $self->do_checkin();
3703 return if $self->bail_out;
3705 unless( $self->permit_override ) {
3707 return if $self->bail_out;
3708 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3709 $self->remove_event('ITEM_NOT_CATALOGED');
3712 $self->override_events;
3713 return if $self->bail_out;
3716 $self->do_checkout();
3721 my( $self, $evt ) = @_;
3722 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3723 $logger->debug("circulator: removing event from list: $evt");
3724 my @events = @{$self->events};
3725 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3730 my( $self, $evt ) = @_;
3731 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3732 return grep { $_->{textcode} eq $evt } @{$self->events};
3736 sub run_renew_permit {
3739 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3740 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3741 $self->editor, $self->copy, $self->editor->requestor, 1
3743 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3746 my $results = $self->run_indb_circ_test;
3747 $self->push_events($self->matrix_test_result_events)
3748 unless $self->circ_test_success;
3752 # XXX: The primary mechanism for storing circ history is now handled
3753 # by tracking real circulation objects instead of bibs in a bucket.
3754 # However, this code is disabled by default and could be useful
3755 # some day, so may as well leave it for now.
3756 sub append_reading_list {
3760 $self->is_checkout and
3766 # verify history is globally enabled and uses the bucket mechanism
3767 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3768 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3770 return undef unless $htype and $htype eq 'bucket';
3772 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3774 # verify the patron wants to retain the hisory
3775 my $setting = $e->search_actor_user_setting(
3776 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3778 unless($setting and $setting->value) {
3783 my $bkt = $e->search_container_copy_bucket(
3784 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3789 # find the next item position
3790 my $last_item = $e->search_container_copy_bucket_item(
3791 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3792 $pos = $last_item->pos + 1 if $last_item;
3795 # create the history bucket if necessary
3796 $bkt = Fieldmapper::container::copy_bucket->new;
3797 $bkt->owner($self->patron->id);
3799 $bkt->btype('circ_history');
3801 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3804 my $item = Fieldmapper::container::copy_bucket_item->new;
3806 $item->bucket($bkt->id);
3807 $item->target_copy($self->copy->id);
3810 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3817 sub make_trigger_events {
3819 return unless $self->circ;
3820 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3821 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3822 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3827 sub checkin_handle_lost_or_lo_now_found {
3828 my ($self, $bill_type, $is_longoverdue) = @_;
3830 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3832 $logger->debug("voiding $tag item billings");
3833 my $result = $CC->void_or_zero_bills_of_type($self->editor, $self->circ, $self->copy, $bill_type, "$tag ITEM RETURNED");
3834 $self->bail_on_events($self->editor->event) if ($result);
3837 sub checkin_handle_lost_or_lo_now_found_restore_od {
3839 my $circ_lib = shift;
3840 my $is_longoverdue = shift;
3841 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3843 # ------------------------------------------------------------------
3844 # restore those overdue charges voided when item was set to lost
3845 # ------------------------------------------------------------------
3847 my $ods = $self->editor->search_money_billing([
3849 xact => $self->circ->id,
3853 order_by => {mb => 'billing_ts desc'}
3857 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
3858 # Because actual users get up to all kinds of unexpectedness, we
3859 # only recreate up to $circ->max_fine in bills. I know you think
3860 # it wouldn't happen that bills could get created, voided, and
3861 # recreated more than once, but I guaran-damn-tee you that it will
3863 if ($ods && @$ods) {
3864 my $void_amount = 0;
3865 my $void_max = $self->circ->max_fine();
3866 # search for overdues voided the new way (aka "adjusted")
3867 my @billings = map {$_->id()} @$ods;
3868 my $voids = $self->editor->search_money_account_adjustment(
3870 billing => \@billings
3874 map {$void_amount += $_->amount()} @$voids;
3876 # if no adjustments found, assume they were voided the old way (aka "voided")
3877 for my $bill (@$ods) {
3878 if( $U->is_true($bill->voided) ) {
3879 $void_amount += $bill->amount();
3885 ($void_amount < $void_max ? $void_amount : $void_max),
3887 $ods->[0]->billing_type(),
3889 "System: $tag RETURNED - OVERDUES REINSTATED",
3890 $ods->[0]->billing_ts() # date this restoration the same as the last overdue (for possible subsequent fine generation)