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 OpenSRF::Utils::Config;
9 use OpenILS::Const qw/:const/;
10 use OpenILS::Application::AppUtils;
12 my $U = "OpenILS::Application::AppUtils";
16 my $opac_renewal_use_circ_lib;
17 my $desk_renewal_use_circ_lib;
19 sub determine_booking_status {
20 unless (defined $booking_status) {
21 my $router_name = OpenSRF::Utils::Config
24 ->router_name || 'router';
26 my $ses = create OpenSRF::AppSession($router_name);
27 $booking_status = grep {$_ eq "open-ils.booking"} @{
28 $ses->request("opensrf.router.info.class.list")->gather(1)
31 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
34 return $booking_status;
40 flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
45 __PACKAGE__->register_method(
46 method => "run_method",
47 api_name => "open-ils.circ.checkout.permit",
49 Determines if the given checkout can occur
50 @param authtoken The login session key
51 @param params A trailing hash of named params including
52 barcode : The copy barcode,
53 patron : The patron the checkout is occurring for,
54 renew : true or false - whether or not this is a renewal
55 @return The event that occurred during the permit check.
59 __PACKAGE__->register_method (
60 method => 'run_method',
61 api_name => 'open-ils.circ.checkout.permit.override',
62 signature => q/@see open-ils.circ.checkout.permit/,
66 __PACKAGE__->register_method(
67 method => "run_method",
68 api_name => "open-ils.circ.checkout",
71 @param authtoken The login session key
72 @param params A named hash of params including:
74 barcode If no copy is provided, the copy is retrieved via barcode
75 copyid If no copy or barcode is provide, the copy id will be use
76 patron The patron's id
77 noncat True if this is a circulation for a non-cataloted item
78 noncat_type The non-cataloged type id
79 noncat_circ_lib The location for the noncat circ.
80 precat The item has yet to be cataloged
81 dummy_title The temporary title of the pre-cataloded item
82 dummy_author The temporary authr of the pre-cataloded item
83 Default is the home org of the staff member
84 @return The SUCCESS event on success, any other event depending on the error
87 __PACKAGE__->register_method(
88 method => "run_method",
89 api_name => "open-ils.circ.checkin",
92 Generic super-method for handling all copies
93 @param authtoken The login session key
94 @param params Hash of named parameters including:
95 barcode - The copy barcode
96 force - If true, copies in bad statuses will be checked in and give good statuses
97 noop - don't capture holds or put items into transit
98 void_overdues - void all overdues for the circulation (aka amnesty)
103 __PACKAGE__->register_method(
104 method => "run_method",
105 api_name => "open-ils.circ.checkin.override",
106 signature => q/@see open-ils.circ.checkin/
109 __PACKAGE__->register_method(
110 method => "run_method",
111 api_name => "open-ils.circ.renew.override",
112 signature => q/@see open-ils.circ.renew/,
116 __PACKAGE__->register_method(
117 method => "run_method",
118 api_name => "open-ils.circ.renew",
119 notes => <<" NOTES");
120 PARAMS( authtoken, circ => circ_id );
121 open-ils.circ.renew(login_session, circ_object);
122 Renews the provided circulation. login_session is the requestor of the
123 renewal and if the logged in user is not the same as circ->usr, then
124 the logged in user must have RENEW_CIRC permissions.
127 __PACKAGE__->register_method(
128 method => "run_method",
129 api_name => "open-ils.circ.checkout.full"
131 __PACKAGE__->register_method(
132 method => "run_method",
133 api_name => "open-ils.circ.checkout.full.override"
135 __PACKAGE__->register_method(
136 method => "run_method",
137 api_name => "open-ils.circ.reservation.pickup"
139 __PACKAGE__->register_method(
140 method => "run_method",
141 api_name => "open-ils.circ.reservation.return"
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.reservation.return.override"
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.inspect",
150 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
155 my( $self, $conn, $auth, $args ) = @_;
156 translate_legacy_args($args);
157 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
158 my $api = $self->api_name;
161 OpenILS::Application::Circ::Circulator->new($auth, %$args);
163 return circ_events($circulator) if $circulator->bail_out;
165 $circulator->use_booking(determine_booking_status());
167 # --------------------------------------------------------------------------
168 # First, check for a booking transit, as the barcode may not be a copy
169 # barcode, but a resource barcode, and nothing else in here will work
170 # --------------------------------------------------------------------------
172 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
173 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
174 if (@$resources) { # yes!
176 my $res_id_list = [ map { $_->id } @$resources ];
177 my $transit = $circulator->editor->search_action_reservation_transit_copy(
179 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef, cancel_time => undef },
180 { order_by => { artc => 'source_send_time' }, limit => 1 }
182 )->[0]; # Any transit for this barcode?
184 if ($transit) { # yes! unwrap it.
186 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
187 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
189 my $success_event = new OpenILS::Event(
190 "SUCCESS", "payload" => {"reservation" => $reservation}
192 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
193 if (my $copy = $circulator->editor->search_asset_copy([
194 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
195 ])->[0]) { # got a copy
196 $copy->status( $transit->copy_status );
197 $copy->editor($circulator->editor->requestor->id);
198 $copy->edit_date('now');
199 $circulator->editor->update_asset_copy($copy);
200 $success_event->{"payload"}->{"record"} =
201 $U->record_to_mvr($copy->call_number->record);
202 $success_event->{"payload"}->{"volume"} = $copy->call_number;
203 $copy->call_number($copy->call_number->id);
204 $success_event->{"payload"}->{"copy"} = $copy;
208 $transit->dest_recv_time('now');
209 $circulator->editor->update_action_reservation_transit_copy( $transit );
211 $circulator->editor->commit;
212 # Formerly this branch just stopped here. Argh!
213 $conn->respond_complete($success_event);
219 if ($circulator->use_booking) {
220 $circulator->is_res_checkin($circulator->is_checkin(1))
221 if $api =~ /reservation.return/ or (
222 $api =~ /checkin/ and $circulator->seems_like_reservation()
225 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
228 $circulator->is_renewal(1) if $api =~ /renew/;
229 $circulator->is_checkin(1) if $api =~ /checkin/;
231 $circulator->mk_env();
232 $circulator->noop(1) if $circulator->claims_never_checked_out;
234 return circ_events($circulator) if $circulator->bail_out;
236 $circulator->override(1) if $api =~ /override/o;
238 if( $api =~ /checkout\.permit/ ) {
239 $circulator->do_permit();
241 } elsif( $api =~ /checkout.full/ ) {
243 # requesting a precat checkout implies that any required
244 # overrides have been performed. Go ahead and re-override.
245 $circulator->skip_permit_key(1);
246 $circulator->override(1) if $circulator->request_precat;
247 $circulator->do_permit();
248 $circulator->is_checkout(1);
249 unless( $circulator->bail_out ) {
250 $circulator->events([]);
251 $circulator->do_checkout();
254 } elsif( $circulator->is_res_checkout ) {
255 $circulator->do_reservation_pickup();
257 } elsif( $api =~ /inspect/ ) {
258 my $data = $circulator->do_inspect();
259 $circulator->editor->rollback;
262 } elsif( $api =~ /checkout/ ) {
263 $circulator->is_checkout(1);
264 $circulator->do_checkout();
266 } elsif( $circulator->is_res_checkin ) {
267 $circulator->do_reservation_return();
268 $circulator->do_checkin() if ($circulator->copy());
269 } elsif( $api =~ /checkin/ ) {
270 $circulator->do_checkin();
272 } elsif( $api =~ /renew/ ) {
273 $circulator->is_renewal(1);
274 $circulator->do_renew();
277 if( $circulator->bail_out ) {
280 # make sure no success event accidentally slip in
282 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
285 my @e = @{$circulator->events};
286 push( @ee, $_->{textcode} ) for @e;
287 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
289 $circulator->editor->rollback;
293 $circulator->editor->commit;
296 $conn->respond_complete(circ_events($circulator));
298 return undef if $circulator->bail_out;
300 $circulator->do_hold_notify($circulator->notify_hold)
301 if $circulator->notify_hold;
302 $circulator->retarget_holds if $circulator->retarget;
303 $circulator->append_reading_list;
304 $circulator->make_trigger_events;
311 my @e = @{$circ->events};
312 # if we have multiple events, SUCCESS should not be one of them;
313 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
314 return (@e == 1) ? $e[0] : \@e;
318 sub translate_legacy_args {
321 if( $$args{barcode} ) {
322 $$args{copy_barcode} = $$args{barcode};
323 delete $$args{barcode};
326 if( $$args{copyid} ) {
327 $$args{copy_id} = $$args{copyid};
328 delete $$args{copyid};
331 if( $$args{patronid} ) {
332 $$args{patron_id} = $$args{patronid};
333 delete $$args{patronid};
336 if( $$args{patron} and !ref($$args{patron}) ) {
337 $$args{patron_id} = $$args{patron};
338 delete $$args{patron};
342 if( $$args{noncat} ) {
343 $$args{is_noncat} = $$args{noncat};
344 delete $$args{noncat};
347 if( $$args{precat} ) {
348 $$args{is_precat} = $$args{request_precat} = $$args{precat};
349 delete $$args{precat};
355 # --------------------------------------------------------------------------
356 # This package actually manages all of the circulation logic
357 # --------------------------------------------------------------------------
358 package OpenILS::Application::Circ::Circulator;
359 use strict; use warnings;
360 use vars q/$AUTOLOAD/;
362 use OpenILS::Utils::Fieldmapper;
363 use OpenSRF::Utils::Cache;
364 use Digest::MD5 qw(md5_hex);
365 use DateTime::Format::ISO8601;
366 use OpenILS::Utils::PermitHold;
367 use OpenSRF::Utils qw/:datetime/;
368 use OpenSRF::Utils::SettingsClient;
369 use OpenILS::Application::Circ::Holds;
370 use OpenILS::Application::Circ::Transit;
371 use OpenSRF::Utils::Logger qw(:logger);
372 use OpenILS::Utils::CStoreEditor qw/:funcs/;
373 use OpenILS::Const qw/:const/;
374 use OpenILS::Utils::Penalty;
375 use OpenILS::Application::Circ::CircCommon;
378 my $CC = "OpenILS::Application::Circ::CircCommon";
379 my $holdcode = "OpenILS::Application::Circ::Holds";
380 my $transcode = "OpenILS::Application::Circ::Transit";
386 # --------------------------------------------------------------------------
387 # Add a pile of automagic getter/setter methods
388 # --------------------------------------------------------------------------
389 my @AUTOLOAD_FIELDS = qw/
435 recurring_fines_level
448 cancelled_hold_transit
455 circ_matrix_matchpoint
466 claims_never_checked_out
479 dont_change_lost_zero
481 needs_lost_bill_handling
487 my $type = ref($self) or die "$self is not an object";
489 my $name = $AUTOLOAD;
492 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
493 $logger->error("circulator: $type: invalid autoload field: $name");
494 die "$type: invalid autoload field: $name\n"
499 *{"${type}::${name}"} = sub {
502 $s->{$name} = $v if defined $v;
506 return $self->$name($data);
511 my( $class, $auth, %args ) = @_;
512 $class = ref($class) || $class;
513 my $self = bless( {}, $class );
516 $self->editor(new_editor(xact => 1, authtoken => $auth));
518 unless( $self->editor->checkauth ) {
519 $self->bail_on_events($self->editor->event);
523 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
525 $self->$_($args{$_}) for keys %args;
528 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
530 # if this is a renewal, default to desk_renewal
531 $self->desk_renewal(1) unless
532 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
534 $self->capture('') unless $self->capture;
536 unless(%user_groups) {
537 my $gps = $self->editor->retrieve_all_permission_grp_tree;
538 %user_groups = map { $_->id => $_ } @$gps;
545 # --------------------------------------------------------------------------
546 # True if we should discontinue processing
547 # --------------------------------------------------------------------------
549 my( $self, $bool ) = @_;
550 if( defined $bool ) {
551 $logger->info("circulator: BAILING OUT") if $bool;
552 $self->{bail_out} = $bool;
554 return $self->{bail_out};
559 my( $self, @evts ) = @_;
562 $e->{payload} = $self->copy if
563 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
565 $logger->info("circulator: pushing event ".$e->{textcode});
566 push( @{$self->events}, $e ) unless
567 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
573 return '' if $self->skip_permit_key;
574 my $key = md5_hex( time() . rand() . "$$" );
575 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
576 return $self->permit_key($key);
579 sub check_permit_key {
581 return 1 if $self->skip_permit_key;
582 my $key = $self->permit_key;
583 return 0 unless $key;
584 my $k = "oils_permit_key_$key";
585 my $one = $self->cache_handle->get_cache($k);
586 $self->cache_handle->delete_cache($k);
587 return ($one) ? 1 : 0;
590 sub seems_like_reservation {
593 # Some words about the following method:
594 # 1) It requires the VIEW_USER permission, but that's not an
595 # issue, right, since all staff should have that?
596 # 2) It returns only one reservation at a time, even if an item can be
597 # and is currently overbooked. Hmmm....
598 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
599 my $result = $booking_ses->request(
600 "open-ils.booking.reservations.by_returnable_resource_barcode",
601 $self->editor->authtoken,
604 $booking_ses->disconnect;
606 return $self->bail_on_events($result) if defined $U->event_code($result);
609 $self->reservation(shift @$result);
617 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
618 sub save_trimmed_copy {
619 my ($self, $copy) = @_;
622 $self->volume($copy->call_number);
623 $self->title($self->volume->record);
624 $self->copy->call_number($self->volume->id);
625 $self->volume->record($self->title->id);
626 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
627 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
628 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
629 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
635 my $e = $self->editor;
637 # --------------------------------------------------------------------------
638 # Grab the fleshed copy
639 # --------------------------------------------------------------------------
640 unless($self->is_noncat) {
643 $copy = $e->retrieve_asset_copy(
644 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
646 } elsif( $self->copy_barcode ) {
648 $copy = $e->search_asset_copy(
649 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
650 } elsif( $self->reservation ) {
651 my $res = $e->json_query(
653 "select" => {"acp" => ["id"]},
658 "field" => "barcode",
662 "field" => "current_resource"
670 "id" => (ref $self->reservation) ?
671 $self->reservation->id : $self->reservation
676 if (ref $res eq "ARRAY" and scalar @$res) {
677 $logger->info("circulator: mapped reservation " .
678 $self->reservation . " to copy " . $res->[0]->{"id"});
679 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
684 $self->save_trimmed_copy($copy);
686 # We can't renew if there is no copy
687 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
688 if $self->is_renewal;
693 # --------------------------------------------------------------------------
695 # --------------------------------------------------------------------------
699 flesh_fields => {au => [ qw/ card / ]}
702 if( $self->patron_id ) {
703 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
704 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
706 } elsif( $self->patron_barcode ) {
708 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
709 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
710 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
712 $patron = $e->retrieve_actor_user($card->usr)
713 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
715 # Use the card we looked up, not the patron's primary, for card active checks
716 $patron->card($card);
719 if( my $copy = $self->copy ) {
722 $flesh->{flesh_fields}->{circ} = ['usr'];
724 my $circ = $e->search_action_circulation([
725 {target_copy => $copy->id, checkin_time => undef}, $flesh
729 $patron = $circ->usr;
730 $circ->usr($patron->id); # de-flesh for consistency
736 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
737 unless $self->patron($patron) or $self->is_checkin;
739 unless($self->is_checkin) {
741 # Check for inactivity and patron reg. expiration
743 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
744 unless $U->is_true($patron->active);
746 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
747 unless $U->is_true($patron->card->active);
749 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
750 cleanse_ISO8601($patron->expire_date));
752 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
753 if( CORE::time > $expire->epoch ) ;
758 # --------------------------------------------------------------------------
759 # Does the circ permit work
760 # --------------------------------------------------------------------------
764 $self->log_me("do_permit()");
766 unless( $self->editor->requestor->id == $self->patron->id ) {
767 return $self->bail_on_events($self->editor->event)
768 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
771 $self->check_captured_holds();
772 $self->do_copy_checks();
773 return if $self->bail_out;
774 $self->run_patron_permit_scripts();
775 $self->run_copy_permit_scripts()
776 unless $self->is_precat or $self->is_noncat;
777 $self->check_item_deposit_events();
778 $self->override_events();
779 return if $self->bail_out;
781 if($self->is_precat and not $self->request_precat) {
784 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
785 return $self->bail_out(1) unless $self->is_renewal;
789 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
792 sub check_item_deposit_events {
794 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
795 if $self->is_deposit and not $self->is_deposit_exempt;
796 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
797 if $self->is_rental and not $self->is_rental_exempt;
800 # returns true if the user is not required to pay deposits
801 sub is_deposit_exempt {
803 my $pid = (ref $self->patron->profile) ?
804 $self->patron->profile->id : $self->patron->profile;
805 my $groups = $U->ou_ancestor_setting_value(
806 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
807 for my $grp (@$groups) {
808 return 1 if $self->is_group_descendant($grp, $pid);
813 # returns true if the user is not required to pay rental fees
814 sub is_rental_exempt {
816 my $pid = (ref $self->patron->profile) ?
817 $self->patron->profile->id : $self->patron->profile;
818 my $groups = $U->ou_ancestor_setting_value(
819 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
820 for my $grp (@$groups) {
821 return 1 if $self->is_group_descendant($grp, $pid);
826 sub is_group_descendant {
827 my($self, $p_id, $c_id) = @_;
828 return 0 unless defined $p_id and defined $c_id;
829 return 1 if $c_id == $p_id;
830 while(my $grp = $user_groups{$c_id}) {
831 $c_id = $grp->parent;
832 return 0 unless defined $c_id;
833 return 1 if $c_id == $p_id;
838 sub check_captured_holds {
840 my $copy = $self->copy;
841 my $patron = $self->patron;
843 return undef unless $copy;
845 my $s = $U->copy_status($copy->status)->id;
846 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
847 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
849 # Item is on the holds shelf, make sure it's going to the right person
850 my $hold = $self->editor->search_action_hold_request(
853 current_copy => $copy->id ,
854 capture_time => { '!=' => undef },
855 cancel_time => undef,
856 fulfillment_time => undef
862 if ($hold and $hold->usr == $patron->id) {
863 $self->checkout_is_for_hold(1);
867 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
869 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
875 my $copy = $self->copy;
878 my $stat = $U->copy_status($copy->status)->id;
880 # We cannot check out a copy if it is in-transit
881 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
882 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
885 $self->handle_claims_returned();
886 return if $self->bail_out;
888 # no claims returned circ was found, check if there is any open circ
889 unless( $self->is_renewal ) {
891 my $circs = $self->editor->search_action_circulation(
892 { target_copy => $copy->id, checkin_time => undef }
895 if(my $old_circ = $circs->[0]) { # an open circ was found
897 my $payload = {copy => $copy};
899 if($old_circ->usr == $self->patron->id) {
901 $payload->{old_circ} = $old_circ;
903 # If there is an open circulation on the checkout item and an auto-renew
904 # interval is defined, inform the caller that they should go
905 # ahead and renew the item instead of warning about open circulations.
907 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
909 'circ.checkout_auto_renew_age',
913 if($auto_renew_intvl) {
914 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
915 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
917 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
918 $payload->{auto_renew} = 1;
923 return $self->bail_on_events(
924 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
930 my $LEGACY_CIRC_EVENT_MAP = {
931 'no_item' => 'ITEM_NOT_CATALOGED',
932 'actor.usr.barred' => 'PATRON_BARRED',
933 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
934 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
935 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
936 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
937 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
938 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
939 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
940 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
941 'config.circ_matrix_test.total_copy_hold_ratio' =>
942 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
943 'config.circ_matrix_test.available_copy_hold_ratio' =>
944 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
948 # ---------------------------------------------------------------------
949 # This pushes any patron-related events into the list but does not
950 # set bail_out for any events
951 # ---------------------------------------------------------------------
952 sub run_patron_permit_scripts {
954 my $patronid = $self->patron->id;
959 my $results = $self->run_indb_circ_test;
960 unless($self->circ_test_success) {
963 if ($self->is_noncat) {
964 # no_item result is OK during noncat checkout
965 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
969 if ($self->checkout_is_for_hold) {
970 # if this checkout will fulfill a hold, ignore CIRC blocks
971 # and rely instead on the (later-checked) FULFILL block
973 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
974 my $fblock_pens = $self->editor->search_config_standing_penalty(
975 {name => [@pen_names], block_list => {like => '%CIRC%'}});
977 for my $res (@$results) {
978 my $name = $res->{fail_part} || '';
979 next if grep {$_->name eq $name} @$fblock_pens;
980 push(@trimmed_results, $res);
984 # not for hold or noncat
985 @trimmed_results = @$results;
989 # update the final set of test results
990 $self->matrix_test_result(\@trimmed_results);
992 push @allevents, $self->matrix_test_result_events;
996 $_->{payload} = $self->copy if
997 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1000 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1002 $self->push_events(@allevents);
1005 sub matrix_test_result_codes {
1007 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1010 sub matrix_test_result_events {
1013 my $event = new OpenILS::Event(
1014 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1016 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1018 } (@{$self->matrix_test_result});
1021 sub run_indb_circ_test {
1023 return $self->matrix_test_result if $self->matrix_test_result;
1025 my $dbfunc = ($self->is_renewal) ?
1026 'action.item_user_renew_test' : 'action.item_user_circ_test';
1028 if( $self->is_precat && $self->request_precat) {
1029 $self->make_precat_copy;
1030 return if $self->bail_out;
1033 my $results = $self->editor->json_query(
1037 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1043 $self->circ_test_success($U->is_true($results->[0]->{success}));
1045 if(my $mp = $results->[0]->{matchpoint}) {
1046 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1047 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1048 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1049 if(defined($results->[0]->{renewals})) {
1050 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1052 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1053 if(defined($results->[0]->{grace_period})) {
1054 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1056 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1057 if(defined($results->[0]->{hard_due_date})) {
1058 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1060 # Grab the *last* response for limit_groups, where it is more likely to be filled
1061 $self->limit_groups($results->[-1]->{limit_groups});
1064 return $self->matrix_test_result($results);
1067 # ---------------------------------------------------------------------
1068 # given a use and copy, this will calculate the circulation policy
1069 # parameters. Only works with in-db circ.
1070 # ---------------------------------------------------------------------
1074 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1076 $self->run_indb_circ_test;
1079 circ_test_success => $self->circ_test_success,
1080 failure_events => [],
1081 failure_codes => [],
1082 matchpoint => $self->circ_matrix_matchpoint
1085 unless($self->circ_test_success) {
1086 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1087 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1090 if($self->circ_matrix_matchpoint) {
1091 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1092 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1093 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1094 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1096 my $policy = $self->get_circ_policy(
1097 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1099 $$results{$_} = $$policy{$_} for keys %$policy;
1105 # ---------------------------------------------------------------------
1106 # Loads the circ policy info for duration, recurring fine, and max
1107 # fine based on the current copy
1108 # ---------------------------------------------------------------------
1109 sub get_circ_policy {
1110 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1113 duration_rule => $duration_rule->name,
1114 recurring_fine_rule => $recurring_fine_rule->name,
1115 max_fine_rule => $max_fine_rule->name,
1116 max_fine => $self->get_max_fine_amount($max_fine_rule),
1117 fine_interval => $recurring_fine_rule->recurrence_interval,
1118 renewal_remaining => $duration_rule->max_renewals,
1119 grace_period => $recurring_fine_rule->grace_period
1122 if($hard_due_date) {
1123 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1124 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1127 $policy->{duration_date_ceiling} = undef;
1128 $policy->{duration_date_ceiling_force} = undef;
1131 $policy->{duration} = $duration_rule->shrt
1132 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1133 $policy->{duration} = $duration_rule->normal
1134 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1135 $policy->{duration} = $duration_rule->extended
1136 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1138 $policy->{recurring_fine} = $recurring_fine_rule->low
1139 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1140 $policy->{recurring_fine} = $recurring_fine_rule->normal
1141 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1142 $policy->{recurring_fine} = $recurring_fine_rule->high
1143 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1148 sub get_max_fine_amount {
1150 my $max_fine_rule = shift;
1151 my $max_amount = $max_fine_rule->amount;
1153 # if is_percent is true then the max->amount is
1154 # use as a percentage of the copy price
1155 if ($U->is_true($max_fine_rule->is_percent)) {
1156 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1157 $max_amount = $price * $max_fine_rule->amount / 100;
1159 $U->ou_ancestor_setting_value(
1161 'circ.max_fine.cap_at_price',
1165 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1166 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1174 sub run_copy_permit_scripts {
1176 my $copy = $self->copy || return;
1180 my $results = $self->run_indb_circ_test;
1181 push @allevents, $self->matrix_test_result_events
1182 unless $self->circ_test_success;
1184 # See if this copy has an alert message
1185 my $ae = $self->check_copy_alert();
1186 push( @allevents, $ae ) if $ae;
1188 # uniquify the events
1189 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1190 @allevents = values %hash;
1192 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1194 $self->push_events(@allevents);
1198 sub check_copy_alert {
1200 return undef if $self->is_renewal;
1201 return OpenILS::Event->new(
1202 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1203 if $self->copy and $self->copy->alert_message;
1209 # --------------------------------------------------------------------------
1210 # If the call is overriding and has permissions to override every collected
1211 # event, the are cleared. Any event that the caller does not have
1212 # permission to override, will be left in the event list and bail_out will
1214 # XXX We need code in here to cancel any holds/transits on copies
1215 # that are being force-checked out
1216 # --------------------------------------------------------------------------
1217 sub override_events {
1219 my @events = @{$self->events};
1220 return unless @events;
1221 my $oargs = $self->override_args;
1223 if(!$self->override) {
1224 return $self->bail_out(1)
1225 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1230 for my $e (@events) {
1231 my $tc = $e->{textcode};
1232 next if $tc eq 'SUCCESS';
1233 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1234 my $ov = "$tc.override";
1235 $logger->info("circulator: attempting to override event: $ov");
1237 return $self->bail_on_events($self->editor->event)
1238 unless( $self->editor->allowed($ov) );
1240 return $self->bail_out(1);
1246 # --------------------------------------------------------------------------
1247 # If there is an open claimsreturn circ on the requested copy, close the
1248 # circ if overriding, otherwise bail out
1249 # --------------------------------------------------------------------------
1250 sub handle_claims_returned {
1252 my $copy = $self->copy;
1254 my $CR = $self->editor->search_action_circulation(
1256 target_copy => $copy->id,
1257 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1258 checkin_time => undef,
1262 return unless ($CR = $CR->[0]);
1266 # - If the caller has set the override flag, we will check the item in
1267 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1269 $CR->checkin_time('now');
1270 $CR->checkin_scan_time('now');
1271 $CR->checkin_lib($self->circ_lib);
1272 $CR->checkin_workstation($self->editor->requestor->wsid);
1273 $CR->checkin_staff($self->editor->requestor->id);
1275 $evt = $self->editor->event
1276 unless $self->editor->update_action_circulation($CR);
1279 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1282 $self->bail_on_events($evt) if $evt;
1287 # --------------------------------------------------------------------------
1288 # This performs the checkout
1289 # --------------------------------------------------------------------------
1293 $self->log_me("do_checkout()");
1295 # make sure perms are good if this isn't a renewal
1296 unless( $self->is_renewal ) {
1297 return $self->bail_on_events($self->editor->event)
1298 unless( $self->editor->allowed('COPY_CHECKOUT') );
1301 # verify the permit key
1302 unless( $self->check_permit_key ) {
1303 if( $self->permit_override ) {
1304 return $self->bail_on_events($self->editor->event)
1305 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1307 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1311 # if this is a non-cataloged circ, build the circ and finish
1312 if( $self->is_noncat ) {
1313 $self->checkout_noncat;
1315 OpenILS::Event->new('SUCCESS',
1316 payload => { noncat_circ => $self->circ }));
1320 if( $self->is_precat ) {
1321 $self->make_precat_copy;
1322 return if $self->bail_out;
1324 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1325 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1328 $self->do_copy_checks;
1329 return if $self->bail_out;
1331 $self->run_checkout_scripts();
1332 return if $self->bail_out;
1334 $self->build_checkout_circ_object();
1335 return if $self->bail_out;
1337 my $modify_to_start = $self->booking_adjusted_due_date();
1338 return if $self->bail_out;
1340 $self->apply_modified_due_date($modify_to_start);
1341 return if $self->bail_out;
1343 return $self->bail_on_events($self->editor->event)
1344 unless $self->editor->create_action_circulation($self->circ);
1346 # refresh the circ to force local time zone for now
1347 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1349 if($self->limit_groups) {
1350 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1353 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1355 return if $self->bail_out;
1357 $self->apply_deposit_fee();
1358 return if $self->bail_out;
1360 $self->handle_checkout_holds();
1361 return if $self->bail_out;
1363 # ------------------------------------------------------------------------------
1364 # Update the patron penalty info in the DB. Run it for permit-overrides
1365 # since the penalties are not updated during the permit phase
1366 # ------------------------------------------------------------------------------
1367 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1369 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1372 if($self->is_renewal) {
1373 # flesh the billing summary for the checked-in circ
1374 $pcirc = $self->editor->retrieve_action_circulation([
1376 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1381 OpenILS::Event->new('SUCCESS',
1383 copy => $U->unflesh_copy($self->copy),
1384 volume => $self->volume,
1385 circ => $self->circ,
1387 holds_fulfilled => $self->fulfilled_holds,
1388 deposit_billing => $self->deposit_billing,
1389 rental_billing => $self->rental_billing,
1390 parent_circ => $pcirc,
1391 patron => ($self->return_patron) ? $self->patron : undef,
1392 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1398 sub apply_deposit_fee {
1400 my $copy = $self->copy;
1402 ($self->is_deposit and not $self->is_deposit_exempt) or
1403 ($self->is_rental and not $self->is_rental_exempt);
1405 return if $self->is_deposit and $self->skip_deposit_fee;
1406 return if $self->is_rental and $self->skip_rental_fee;
1408 my $bill = Fieldmapper::money::billing->new;
1409 my $amount = $copy->deposit_amount;
1413 if($self->is_deposit) {
1414 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1416 $self->deposit_billing($bill);
1418 $billing_type = OILS_BILLING_TYPE_RENTAL;
1420 $self->rental_billing($bill);
1423 $bill->xact($self->circ->id);
1424 $bill->amount($amount);
1425 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1426 $bill->billing_type($billing_type);
1427 $bill->btype($btype);
1428 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1430 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1435 my $copy = $self->copy;
1437 my $stat = $copy->status if ref $copy->status;
1438 my $loc = $copy->location if ref $copy->location;
1439 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1441 $copy->status($stat->id) if $stat;
1442 $copy->location($loc->id) if $loc;
1443 $copy->circ_lib($circ_lib->id) if $circ_lib;
1444 $copy->editor($self->editor->requestor->id);
1445 $copy->edit_date('now');
1446 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1448 return $self->bail_on_events($self->editor->event)
1449 unless $self->editor->update_asset_copy($self->copy);
1451 $copy->status($U->copy_status($copy->status));
1452 $copy->location($loc) if $loc;
1453 $copy->circ_lib($circ_lib) if $circ_lib;
1456 sub update_reservation {
1458 my $reservation = $self->reservation;
1460 my $usr = $reservation->usr;
1461 my $target_rt = $reservation->target_resource_type;
1462 my $target_r = $reservation->target_resource;
1463 my $current_r = $reservation->current_resource;
1465 $reservation->usr($usr->id) if ref $usr;
1466 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1467 $reservation->target_resource($target_r->id) if ref $target_r;
1468 $reservation->current_resource($current_r->id) if ref $current_r;
1470 return $self->bail_on_events($self->editor->event)
1471 unless $self->editor->update_booking_reservation($self->reservation);
1474 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1475 $self->reservation($reservation);
1479 sub bail_on_events {
1480 my( $self, @evts ) = @_;
1481 $self->push_events(@evts);
1485 # ------------------------------------------------------------------------------
1486 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1487 # affects copies that will fulfill holds and CIRC affects all other copies.
1488 # If blocks exists, bail, push Events onto the event pile, and return true.
1489 # ------------------------------------------------------------------------------
1490 sub check_hold_fulfill_blocks {
1493 # With the addition of ignore_proximity in csp, we need to fetch
1494 # the proximity of both the circ_lib and the copy's circ_lib to
1495 # the patron's home_ou.
1496 my ($ou_prox, $copy_prox);
1497 my $home_ou = (ref($self->patron->home_ou)) ? $self->patron->home_ou->id : $self->patron->home_ou;
1498 $ou_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $self->circ_lib);
1499 $ou_prox = -1 unless (defined($ou_prox));
1500 my $copy_ou = (ref($self->copy->circ_lib)) ? $self->copy->circ_lib->id : $self->copy->circ_lib;
1501 if ($copy_ou == $self->circ_lib) {
1502 # Save us the time of an extra query.
1503 $copy_prox = $ou_prox;
1505 $copy_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $copy_ou);
1506 $copy_prox = -1 unless (defined($copy_prox));
1509 # See if the user has any penalties applied that prevent hold fulfillment
1510 my $pens = $self->editor->json_query({
1511 select => {csp => ['name', 'label']},
1512 from => {ausp => {csp => {}}},
1515 usr => $self->patron->id,
1516 org_unit => $U->get_org_full_path($self->circ_lib),
1518 {stop_date => undef},
1519 {stop_date => {'>' => 'now'}}
1523 block_list => {'like' => '%FULFILL%'},
1525 {ignore_proximity => undef},
1526 {ignore_proximity => {'<' => $ou_prox}},
1527 {ignore_proximity => {'<' => $copy_prox}}
1533 return 0 unless @$pens;
1535 for my $pen (@$pens) {
1536 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1537 my $event = OpenILS::Event->new($pen->{name});
1538 $event->{desc} = $pen->{label};
1539 $self->push_events($event);
1542 $self->override_events;
1543 return $self->bail_out;
1547 # ------------------------------------------------------------------------------
1548 # When an item is checked out, see if we can fulfill a hold for this patron
1549 # ------------------------------------------------------------------------------
1550 sub handle_checkout_holds {
1552 my $copy = $self->copy;
1553 my $patron = $self->patron;
1555 my $e = $self->editor;
1556 $self->fulfilled_holds([]);
1558 # non-cats can't fulfill a hold
1559 return if $self->is_noncat;
1561 my $hold = $e->search_action_hold_request({
1562 current_copy => $copy->id ,
1563 cancel_time => undef,
1564 fulfillment_time => undef
1567 if($hold and $hold->usr != $patron->id) {
1568 # reset the hold since the copy is now checked out
1570 $logger->info("circulator: un-targeting hold ".$hold->id.
1571 " because copy ".$copy->id." is getting checked out");
1573 $hold->clear_prev_check_time;
1574 $hold->clear_current_copy;
1575 $hold->clear_capture_time;
1576 $hold->clear_shelf_time;
1577 $hold->clear_shelf_expire_time;
1578 $hold->clear_current_shelf_lib;
1580 return $self->bail_on_event($e->event)
1581 unless $e->update_action_hold_request($hold);
1587 $hold = $self->find_related_user_hold($copy, $patron) or return;
1588 $logger->info("circulator: found related hold to fulfill in checkout");
1591 return if $self->check_hold_fulfill_blocks;
1593 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1595 # if the hold was never officially captured, capture it.
1596 $hold->current_copy($copy->id);
1597 $hold->capture_time('now') unless $hold->capture_time;
1598 $hold->fulfillment_time('now');
1599 $hold->fulfillment_staff($e->requestor->id);
1600 $hold->fulfillment_lib($self->circ_lib);
1602 return $self->bail_on_events($e->event)
1603 unless $e->update_action_hold_request($hold);
1605 return $self->fulfilled_holds([$hold->id]);
1609 # ------------------------------------------------------------------------------
1610 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1611 # the patron directly targets the checked out item, see if there is another hold
1612 # for the patron that could be fulfilled by the checked out item. Fulfill the
1613 # oldest hold and only fulfill 1 of them.
1615 # For "another hold":
1617 # First, check for one that the copy matches via hold_copy_map, ensuring that
1618 # *any* hold type that this copy could fill may end up filled.
1620 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1621 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1622 # that are non-requestable to count as capturing those hold types.
1623 # ------------------------------------------------------------------------------
1624 sub find_related_user_hold {
1625 my($self, $copy, $patron) = @_;
1626 my $e = $self->editor;
1628 # holds on precat copies are always copy-level, so this call will
1629 # always return undef. Exit early.
1630 return undef if $self->is_precat;
1632 return undef unless $U->ou_ancestor_setting_value(
1633 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1635 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1637 select => {ahr => ['id']},
1646 fkey => 'current_copy',
1647 type => 'left' # there may be no current_copy
1654 fulfillment_time => undef,
1655 cancel_time => undef,
1657 {expire_time => undef},
1658 {expire_time => {'>' => 'now'}}
1662 target_copy => $self->copy->id
1666 {id => undef}, # left-join copy may be nonexistent
1667 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1671 order_by => {ahr => {request_time => {direction => 'asc'}}},
1675 my $hold_info = $e->json_query($args)->[0];
1676 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1677 return undef if $U->ou_ancestor_setting_value(
1678 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1680 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1682 select => {ahr => ['id']},
1687 fkey => 'current_copy',
1688 type => 'left' # there may be no current_copy
1695 fulfillment_time => undef,
1696 cancel_time => undef,
1698 {expire_time => undef},
1699 {expire_time => {'>' => 'now'}}
1706 target => $self->volume->id
1712 target => $self->title->id
1718 {id => undef}, # left-join copy may be nonexistent
1719 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1723 order_by => {ahr => {request_time => {direction => 'asc'}}},
1727 $hold_info = $e->json_query($args)->[0];
1728 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1733 sub run_checkout_scripts {
1746 my $hard_due_date_name;
1748 $self->run_indb_circ_test();
1749 $duration = $self->circ_matrix_matchpoint->duration_rule;
1750 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1751 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1752 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1754 $duration_name = $duration->name if $duration;
1755 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1758 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1759 return $self->bail_on_events($evt) if ($evt && !$nobail);
1761 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1762 return $self->bail_on_events($evt) if ($evt && !$nobail);
1764 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1765 return $self->bail_on_events($evt) if ($evt && !$nobail);
1767 if($hard_due_date_name) {
1768 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1769 return $self->bail_on_events($evt) if ($evt && !$nobail);
1775 # The item circulates with an unlimited duration
1779 $hard_due_date = undef;
1782 $self->duration_rule($duration);
1783 $self->recurring_fines_rule($recurring);
1784 $self->max_fine_rule($max_fine);
1785 $self->hard_due_date($hard_due_date);
1789 sub build_checkout_circ_object {
1792 my $circ = Fieldmapper::action::circulation->new;
1793 my $duration = $self->duration_rule;
1794 my $max = $self->max_fine_rule;
1795 my $recurring = $self->recurring_fines_rule;
1796 my $hard_due_date = $self->hard_due_date;
1797 my $copy = $self->copy;
1798 my $patron = $self->patron;
1799 my $duration_date_ceiling;
1800 my $duration_date_ceiling_force;
1804 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1805 $duration_date_ceiling = $policy->{duration_date_ceiling};
1806 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1808 my $dname = $duration->name;
1809 my $mname = $max->name;
1810 my $rname = $recurring->name;
1812 if($hard_due_date) {
1813 $hdname = $hard_due_date->name;
1816 $logger->debug("circulator: building circulation ".
1817 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1819 $circ->duration($policy->{duration});
1820 $circ->recurring_fine($policy->{recurring_fine});
1821 $circ->duration_rule($duration->name);
1822 $circ->recurring_fine_rule($recurring->name);
1823 $circ->max_fine_rule($max->name);
1824 $circ->max_fine($policy->{max_fine});
1825 $circ->fine_interval($recurring->recurrence_interval);
1826 $circ->renewal_remaining($duration->max_renewals);
1827 $circ->grace_period($policy->{grace_period});
1831 $logger->info("circulator: copy found with an unlimited circ duration");
1832 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1833 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1834 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1835 $circ->renewal_remaining(0);
1836 $circ->grace_period(0);
1839 $circ->target_copy( $copy->id );
1840 $circ->usr( $patron->id );
1841 $circ->circ_lib( $self->circ_lib );
1842 $circ->workstation($self->editor->requestor->wsid)
1843 if defined $self->editor->requestor->wsid;
1845 # renewals maintain a link to the parent circulation
1846 $circ->parent_circ($self->parent_circ);
1848 if( $self->is_renewal ) {
1849 $circ->opac_renewal('t') if $self->opac_renewal;
1850 $circ->phone_renewal('t') if $self->phone_renewal;
1851 $circ->desk_renewal('t') if $self->desk_renewal;
1852 $circ->renewal_remaining($self->renewal_remaining);
1853 $circ->circ_staff($self->editor->requestor->id);
1857 # if the user provided an overiding checkout time,
1858 # (e.g. the checkout really happened several hours ago), then
1859 # we apply that here. Does this need a perm??
1860 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1861 if $self->checkout_time;
1863 # if a patron is renewing, 'requestor' will be the patron
1864 $circ->circ_staff($self->editor->requestor->id);
1865 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
1870 sub do_reservation_pickup {
1873 $self->log_me("do_reservation_pickup()");
1875 $self->reservation->pickup_time('now');
1878 $self->reservation->current_resource &&
1879 $U->is_true($self->reservation->target_resource_type->catalog_item)
1881 # We used to try to set $self->copy and $self->patron here,
1882 # but that should already be done.
1884 $self->run_checkout_scripts(1);
1886 my $duration = $self->duration_rule;
1887 my $max = $self->max_fine_rule;
1888 my $recurring = $self->recurring_fines_rule;
1890 if ($duration && $max && $recurring) {
1891 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1893 my $dname = $duration->name;
1894 my $mname = $max->name;
1895 my $rname = $recurring->name;
1897 $logger->debug("circulator: updating reservation ".
1898 "with duration=$dname, maxfine=$mname, recurring=$rname");
1900 $self->reservation->fine_amount($policy->{recurring_fine});
1901 $self->reservation->max_fine($policy->{max_fine});
1902 $self->reservation->fine_interval($recurring->recurrence_interval);
1905 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1906 $self->update_copy();
1909 $self->reservation->fine_amount(
1910 $self->reservation->target_resource_type->fine_amount
1912 $self->reservation->max_fine(
1913 $self->reservation->target_resource_type->max_fine
1915 $self->reservation->fine_interval(
1916 $self->reservation->target_resource_type->fine_interval
1920 $self->update_reservation();
1923 sub do_reservation_return {
1925 my $request = shift;
1927 $self->log_me("do_reservation_return()");
1929 if (not ref $self->reservation) {
1930 my ($reservation, $evt) =
1931 $U->fetch_booking_reservation($self->reservation);
1932 return $self->bail_on_events($evt) if $evt;
1933 $self->reservation($reservation);
1936 $self->handle_fines(1);
1937 $self->reservation->return_time('now');
1938 $self->update_reservation();
1939 $self->reshelve_copy if $self->copy;
1941 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1942 $self->copy( $self->reservation->current_resource->catalog_item );
1946 sub booking_adjusted_due_date {
1948 my $circ = $self->circ;
1949 my $copy = $self->copy;
1951 return undef unless $self->use_booking;
1955 if( $self->due_date ) {
1957 return $self->bail_on_events($self->editor->event)
1958 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1960 $circ->due_date(cleanse_ISO8601($self->due_date));
1964 return unless $copy and $circ->due_date;
1967 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1968 if (@$booking_items) {
1969 my $booking_item = $booking_items->[0];
1970 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1972 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1973 my $shorten_circ_setting = $resource_type->elbow_room ||
1974 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1977 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1978 my $bookings = $booking_ses->request('open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken, {
1979 resource => $booking_item->id
1980 , search_start => 'now'
1981 , search_end => $circ->due_date
1982 , fields => { cancel_time => undef, return_time => undef }
1984 $booking_ses->disconnect;
1986 throw OpenSRF::EX::ERROR ("Improper input arguments") unless defined $bookings;
1987 return $self->bail_on_events($bookings) if ref($bookings) eq 'HASH';
1989 my $dt_parser = DateTime::Format::ISO8601->new;
1990 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1992 for my $bid (@$bookings) {
1994 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1996 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1997 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1999 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2000 if ($booking_start < DateTime->now);
2003 if ($U->is_true($stop_circ_setting)) {
2004 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2006 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2007 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2010 # We set the circ duration here only to affect the logic that will
2011 # later (in a DB trigger) mangle the time part of the due date to
2012 # 11:59pm. Having any circ duration that is not a whole number of
2013 # days is enough to prevent the "correction."
2014 my $new_circ_duration = $due_date->epoch - time;
2015 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2016 $circ->duration("$new_circ_duration seconds");
2018 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2022 return $self->bail_on_events($self->editor->event)
2023 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2029 sub apply_modified_due_date {
2031 my $shift_earlier = shift;
2032 my $circ = $self->circ;
2033 my $copy = $self->copy;
2035 if( $self->due_date ) {
2037 return $self->bail_on_events($self->editor->event)
2038 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2040 $circ->due_date(cleanse_ISO8601($self->due_date));
2044 # if the due_date lands on a day when the location is closed
2045 return unless $copy and $circ->due_date;
2047 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2049 # due-date overlap should be determined by the location the item
2050 # is checked out from, not the owning or circ lib of the item
2051 my $org = $self->circ_lib;
2053 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2054 " with an item due date of ".$circ->due_date );
2056 my $dateinfo = $U->storagereq(
2057 'open-ils.storage.actor.org_unit.closed_date.overlap',
2058 $org, $circ->due_date );
2061 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2062 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2064 # XXX make the behavior more dynamic
2065 # for now, we just push the due date to after the close date
2066 if ($shift_earlier) {
2067 $circ->due_date($dateinfo->{start});
2069 $circ->due_date($dateinfo->{end});
2077 sub create_due_date {
2078 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2080 # if there is a raw time component (e.g. from postgres),
2081 # turn it into an interval that interval_to_seconds can parse
2082 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2084 # for now, use the server timezone. TODO: use workstation org timezone
2085 my $due_date = DateTime->now(time_zone => 'local');
2086 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2088 # add the circ duration
2089 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2092 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2093 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2094 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2099 # return ISO8601 time with timezone
2100 return $due_date->strftime('%FT%T%z');
2105 sub make_precat_copy {
2107 my $copy = $self->copy;
2110 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2112 $copy->editor($self->editor->requestor->id);
2113 $copy->edit_date('now');
2114 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2115 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2116 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2117 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2118 $self->update_copy();
2122 $logger->info("circulator: Creating a new precataloged ".
2123 "copy in checkout with barcode " . $self->copy_barcode);
2125 $copy = Fieldmapper::asset::copy->new;
2126 $copy->circ_lib($self->circ_lib);
2127 $copy->creator($self->editor->requestor->id);
2128 $copy->editor($self->editor->requestor->id);
2129 $copy->barcode($self->copy_barcode);
2130 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2131 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2132 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2134 $copy->dummy_title($self->dummy_title || "");
2135 $copy->dummy_author($self->dummy_author || "");
2136 $copy->dummy_isbn($self->dummy_isbn || "");
2137 $copy->circ_modifier($self->circ_modifier);
2140 # See if we need to override the circ_lib for the copy with a configured circ_lib
2141 # Setting is shortname of the org unit
2142 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2143 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2145 if($precat_circ_lib) {
2146 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2149 $self->bail_on_events($self->editor->event);
2153 $copy->circ_lib($org->id);
2157 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2159 $self->push_events($self->editor->event);
2165 sub checkout_noncat {
2171 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2172 my $count = $self->noncat_count || 1;
2173 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2175 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2179 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2180 $self->editor->requestor->id,
2188 $self->push_events($evt);
2196 # If a copy goes into transit and is then checked in before the transit checkin
2197 # interval has expired, push an event onto the overridable events list.
2198 sub check_transit_checkin_interval {
2201 # only concerned with in-transit items
2202 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2204 # no interval, no problem
2205 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2206 return unless $interval;
2208 # capture the transit so we don't have to fetch it again later during checkin
2210 $self->editor->search_action_transit_copy(
2211 {target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef}
2215 # transit from X to X for whatever reason has no min interval
2216 return if $self->transit->source == $self->transit->dest;
2218 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2219 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2220 my $horizon = $t_start->add(seconds => $seconds);
2222 # See if we are still within the transit checkin forbidden range
2223 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2224 if $horizon > DateTime->now;
2227 # Retarget local holds at checkin
2228 sub checkin_retarget {
2230 return unless $self->retarget_mode and $self->retarget_mode =~ m/retarget/; # Retargeting?
2231 return unless $self->is_checkin; # Renewals need not be checked
2232 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2233 return if $self->is_precat; # No holds for precats
2234 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2235 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2236 my $status = $U->copy_status($self->copy->status);
2237 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2238 # Specifically target items that are likely new (by status ID)
2239 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2240 my $location = $self->copy->location;
2241 if(!ref($location)) {
2242 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2243 $self->copy->location($location);
2245 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2247 # Fetch holds for the bib
2248 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2249 $self->editor->authtoken,
2252 capture_time => undef, # No touching captured holds
2253 frozen => 'f', # Don't bother with frozen holds
2254 pickup_lib => $self->circ_lib # Only holds actually here
2257 # Error? Skip the step.
2258 return if exists $result->{"ilsevent"};
2262 foreach my $holdlist (keys %{$result}) {
2263 push @$holds, @{$result->{$holdlist}};
2266 return if scalar(@$holds) == 0; # No holds, no retargeting
2268 # Check for parts on this copy
2269 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2270 my %parts_hash = ();
2271 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2273 # Loop over holds in request-ish order
2274 # Stage 1: Get them into request-ish order
2275 # Also grab type and target for skipping low hanging ones
2276 $result = $self->editor->json_query({
2277 "select" => { "ahr" => ["id", "hold_type", "target"] },
2278 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2279 "where" => { "id" => $holds },
2281 { "class" => "pgt", "field" => "hold_priority"},
2282 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2283 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2284 { "class" => "ahr", "field" => "request_time"}
2289 if (ref $result eq "ARRAY" and scalar @$result) {
2290 foreach (@{$result}) {
2291 # Copy level, but not this copy?
2292 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2293 and $_->{target} != $self->copy->id);
2294 # Volume level, but not this volume?
2295 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2296 if(@$parts) { # We have parts?
2298 next if ($_->{hold_type} eq 'T');
2299 # Skip part holds for parts not on this copy
2300 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2302 # No parts, no part holds
2303 next if ($_->{hold_type} eq 'P');
2305 # So much for easy stuff, attempt a retarget!
2306 my $tresult = $U->simplereq(
2307 'open-ils.hold-targeter',
2308 'open-ils.hold-targeter.target',
2309 {hold => $_->{id}, find_copy => $self->copy->id}
2311 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2312 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2320 $self->log_me("do_checkin()");
2322 return $self->bail_on_events(
2323 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2326 $self->check_transit_checkin_interval;
2327 $self->checkin_retarget;
2329 # the renew code and mk_env should have already found our circulation object
2330 unless( $self->circ ) {
2332 my $circs = $self->editor->search_action_circulation(
2333 { target_copy => $self->copy->id, checkin_time => undef });
2335 $self->circ($$circs[0]);
2337 # for now, just warn if there are multiple open circs on a copy
2338 $logger->warn("circulator: we have ".scalar(@$circs).
2339 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2342 my $stat = $U->copy_status($self->copy->status)->id;
2344 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2345 # differently if they are already paid for. We need to check for this
2346 # early since overdue generation is potentially affected.
2347 my $dont_change_lost_zero = 0;
2348 if ($stat == OILS_COPY_STATUS_LOST
2349 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2350 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2352 # LOST fine settings are controlled by the copy's circ lib, not the the
2354 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2355 $self->copy->circ_lib->id : $self->copy->circ_lib;
2356 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2357 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2358 $self->editor) || 0;
2360 if ($dont_change_lost_zero) {
2361 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2362 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2365 $self->dont_change_lost_zero($dont_change_lost_zero);
2368 if( $self->checkin_check_holds_shelf() ) {
2369 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2370 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2371 if($self->fake_hold_dest) {
2372 $self->hold->pickup_lib($self->circ_lib);
2374 $self->checkin_flesh_events;
2378 unless( $self->is_renewal ) {
2379 return $self->bail_on_events($self->editor->event)
2380 unless $self->editor->allowed('COPY_CHECKIN');
2383 $self->push_events($self->check_copy_alert());
2384 $self->push_events($self->check_checkin_copy_status());
2386 # if the circ is marked as 'claims returned', add the event to the list
2387 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2388 if ($self->circ and $self->circ->stop_fines
2389 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2391 $self->check_circ_deposit();
2393 # handle the overridable events
2394 $self->override_events unless $self->is_renewal;
2395 return if $self->bail_out;
2397 if( $self->copy and !$self->transit ) {
2399 $self->editor->search_action_transit_copy(
2400 { target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef }
2406 $self->checkin_handle_circ_start;
2407 return if $self->bail_out;
2409 if (!$dont_change_lost_zero) {
2410 # if this circ is LOST and we are configured to generate overdue
2411 # fines for lost items on checkin (to fill the gap between mark
2412 # lost time and when the fines would have naturally stopped), then
2413 # stop_fines is no longer valid and should be cleared.
2415 # stop_fines will be set again during the handle_fines() stage.
2416 # XXX should this setting come from the copy circ lib (like other
2417 # LOST settings), instead of the circulation circ lib?
2418 if ($stat == OILS_COPY_STATUS_LOST) {
2419 $self->circ->clear_stop_fines if
2420 $U->ou_ancestor_setting_value(
2422 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2427 # Set stop_fines when claimed never checked out
2428 $self->circ->stop_fines( OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT ) if( $self->claims_never_checked_out );
2430 # handle fines for this circ, including overdue gen if needed
2431 $self->handle_fines;
2434 $self->checkin_handle_circ_finish;
2435 return if $self->bail_out;
2436 $self->checkin_changed(1);
2438 } elsif( $self->transit ) {
2439 my $hold_transit = $self->process_received_transit;
2440 $self->checkin_changed(1);
2442 if( $self->bail_out ) {
2443 $self->checkin_flesh_events;
2447 if( my $e = $self->check_checkin_copy_status() ) {
2448 # If the original copy status is special, alert the caller
2449 my $ev = $self->events;
2450 $self->events([$e]);
2451 $self->override_events;
2452 return if $self->bail_out;
2456 if( $hold_transit or
2457 $U->copy_status($self->copy->status)->id
2458 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2461 if( $hold_transit ) {
2462 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2464 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2469 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2471 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2472 $self->reshelve_copy(1);
2473 $self->cancelled_hold_transit(1);
2474 $self->notify_hold(0); # don't notify for cancelled holds
2475 $self->fake_hold_dest(0);
2476 return if $self->bail_out;
2478 } elsif ($hold and $hold->hold_type eq 'R') {
2480 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2481 $self->notify_hold(0); # No need to notify
2482 $self->fake_hold_dest(0);
2483 $self->noop(1); # Don't try and capture for other holds/transits now
2484 $self->update_copy();
2485 $hold->fulfillment_time('now');
2486 $self->bail_on_events($self->editor->event)
2487 unless $self->editor->update_action_hold_request($hold);
2491 # hold transited to correct location
2492 if($self->fake_hold_dest) {
2493 $hold->pickup_lib($self->circ_lib);
2495 $self->checkin_flesh_events;
2500 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2502 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2503 " that is in-transit, but there is no transit.. repairing");
2504 $self->reshelve_copy(1);
2505 return if $self->bail_out;
2508 if( $self->is_renewal ) {
2509 $self->finish_fines_and_voiding;
2510 return if $self->bail_out;
2511 $self->push_events(OpenILS::Event->new('SUCCESS'));
2515 # ------------------------------------------------------------------------------
2516 # Circulations and transits are now closed where necessary. Now go on to see if
2517 # this copy can fulfill a hold or needs to be routed to a different location
2518 # ------------------------------------------------------------------------------
2520 my $needed_for_something = 0; # formerly "needed_for_hold"
2522 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2524 if (!$self->remote_hold) {
2525 if ($self->use_booking) {
2526 my $potential_hold = $self->hold_capture_is_possible;
2527 my $potential_reservation = $self->reservation_capture_is_possible;
2529 if ($potential_hold and $potential_reservation) {
2530 $logger->info("circulator: item could fulfill either hold or reservation");
2531 $self->push_events(new OpenILS::Event(
2532 "HOLD_RESERVATION_CONFLICT",
2533 "hold" => $potential_hold,
2534 "reservation" => $potential_reservation
2536 return if $self->bail_out;
2537 } elsif ($potential_hold) {
2538 $needed_for_something =
2539 $self->attempt_checkin_hold_capture;
2540 } elsif ($potential_reservation) {
2541 $needed_for_something =
2542 $self->attempt_checkin_reservation_capture;
2545 $needed_for_something = $self->attempt_checkin_hold_capture;
2548 return if $self->bail_out;
2550 unless($needed_for_something) {
2551 my $circ_lib = (ref $self->copy->circ_lib) ?
2552 $self->copy->circ_lib->id : $self->copy->circ_lib;
2554 if( $self->remote_hold ) {
2555 $circ_lib = $self->remote_hold->pickup_lib;
2556 $logger->warn("circulator: Copy ".$self->copy->barcode.
2557 " is on a remote hold's shelf, sending to $circ_lib");
2560 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2562 my $suppress_transit = 0;
2564 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2565 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2566 if($suppress_transit_source && $suppress_transit_source->{value}) {
2567 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2568 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2569 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2570 $suppress_transit = 1;
2575 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2576 # copy is where it needs to be, either for hold or reshelving
2578 $self->checkin_handle_precat();
2579 return if $self->bail_out;
2582 # copy needs to transit "home", or stick here if it's a floating copy
2584 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2585 my $res = $self->editor->json_query(
2587 'evergreen.can_float',
2588 $self->copy->floating->id,
2589 $self->copy->circ_lib,
2594 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2596 if ($can_float) { # Yep, floating, stick here
2597 $self->checkin_changed(1);
2598 $self->copy->circ_lib( $self->circ_lib );
2601 my $bc = $self->copy->barcode;
2602 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2603 $self->checkin_build_copy_transit($circ_lib);
2604 return if $self->bail_out;
2605 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2609 } else { # no-op checkin
2610 if ($self->copy->floating) { # XXX floating items still stick where they are even with no-op checkin?
2611 my $res = $self->editor->json_query(
2614 'evergreen.can_float',
2615 $self->copy->floating->id,
2616 $self->copy->circ_lib,
2621 if ($res && @$res && $U->is_true($res->[0]->{'evergreen.can_float'})) {
2622 $self->checkin_changed(1);
2623 $self->copy->circ_lib( $self->circ_lib );
2629 if($self->claims_never_checked_out and
2630 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2632 # the item was not supposed to be checked out to the user and should now be marked as missing
2633 $self->copy->status(OILS_COPY_STATUS_MISSING);
2637 $self->reshelve_copy unless $needed_for_something;
2640 return if $self->bail_out;
2642 unless($self->checkin_changed) {
2644 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2645 my $stat = $U->copy_status($self->copy->status)->id;
2647 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2648 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2649 $self->bail_out(1); # no need to commit anything
2653 $self->push_events(OpenILS::Event->new('SUCCESS'))
2654 unless @{$self->events};
2657 $self->finish_fines_and_voiding;
2659 OpenILS::Utils::Penalty->calculate_penalties(
2660 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2662 $self->checkin_flesh_events;
2666 sub finish_fines_and_voiding {
2668 return unless $self->circ;
2670 return unless $self->backdate or $self->void_overdues;
2672 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2673 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2675 my $evt = $CC->void_or_zero_overdues(
2676 $self->editor, $self->circ, {backdate => $self->void_overdues ? undef : $self->backdate, note => $note});
2678 return $self->bail_on_events($evt) if $evt;
2680 # Make sure the circ is open or closed as necessary.
2681 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2682 return $self->bail_on_events($evt) if $evt;
2688 # if a deposit was payed for this item, push the event
2689 sub check_circ_deposit {
2691 return unless $self->circ;
2692 my $deposit = $self->editor->search_money_billing(
2694 xact => $self->circ->id,
2696 }, {idlist => 1})->[0];
2698 $self->push_events(OpenILS::Event->new(
2699 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2704 my $force = $self->force || shift;
2705 my $copy = $self->copy;
2707 my $stat = $U->copy_status($copy->status)->id;
2710 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2711 $stat != OILS_COPY_STATUS_CATALOGING and
2712 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2713 $stat != OILS_COPY_STATUS_RESHELVING )) {
2715 $copy->status( OILS_COPY_STATUS_RESHELVING );
2717 $self->checkin_changed(1);
2722 # Returns true if the item is at the current location
2723 # because it was transited there for a hold and the
2724 # hold has not been fulfilled
2725 sub checkin_check_holds_shelf {
2727 return 0 unless $self->copy;
2730 $U->copy_status($self->copy->status)->id ==
2731 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2733 # Attempt to clear shelf expired holds for this copy
2734 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2735 if($self->clear_expired);
2737 # find the hold that put us on the holds shelf
2738 my $holds = $self->editor->search_action_hold_request(
2740 current_copy => $self->copy->id,
2741 capture_time => { '!=' => undef },
2742 fulfillment_time => undef,
2743 cancel_time => undef,
2748 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2749 $self->reshelve_copy(1);
2753 my $hold = $$holds[0];
2755 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2756 $hold->id. "] for copy ".$self->copy->barcode);
2758 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2759 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2760 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2761 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2762 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2763 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2764 $self->fake_hold_dest(1);
2770 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2771 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2775 $logger->info("circulator: hold is not for here..");
2776 $self->remote_hold($hold);
2781 sub checkin_handle_precat {
2783 my $copy = $self->copy;
2785 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2786 $copy->status(OILS_COPY_STATUS_CATALOGING);
2787 $self->update_copy();
2788 $self->checkin_changed(1);
2789 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2794 sub checkin_build_copy_transit {
2797 my $copy = $self->copy;
2798 my $transit = Fieldmapper::action::transit_copy->new;
2800 # if we are transiting an item to the shelf shelf, it's a hold transit
2801 if (my $hold = $self->remote_hold) {
2802 $transit = Fieldmapper::action::hold_transit_copy->new;
2803 $transit->hold($hold->id);
2805 # the item is going into transit, remove any shelf-iness
2806 if ($hold->current_shelf_lib or $hold->shelf_time) {
2807 $hold->clear_current_shelf_lib;
2808 $hold->clear_shelf_time;
2809 return $self->bail_on_events($self->editor->event)
2810 unless $self->editor->update_action_hold_request($hold);
2814 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2815 $logger->info("circulator: transiting copy to $dest");
2817 $transit->source($self->circ_lib);
2818 $transit->dest($dest);
2819 $transit->target_copy($copy->id);
2820 $transit->source_send_time('now');
2821 $transit->copy_status( $U->copy_status($copy->status)->id );
2823 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2825 if ($self->remote_hold) {
2826 return $self->bail_on_events($self->editor->event)
2827 unless $self->editor->create_action_hold_transit_copy($transit);
2829 return $self->bail_on_events($self->editor->event)
2830 unless $self->editor->create_action_transit_copy($transit);
2833 # ensure the transit is returned to the caller
2834 $self->transit($transit);
2836 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2838 $self->checkin_changed(1);
2842 sub hold_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 # See if this copy can fulfill any holds
2850 my $hold = $holdcode->find_nearest_permitted_hold(
2851 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2853 return undef if ref $hold eq "HASH" and
2854 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2858 sub reservation_capture_is_possible {
2860 my $copy = $self->copy;
2862 # we've been explicitly told not to capture any holds
2863 return 0 if $self->capture eq 'nocapture';
2865 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2866 my $resv = $booking_ses->request(
2867 "open-ils.booking.reservations.could_capture",
2868 $self->editor->authtoken, $copy->barcode
2870 $booking_ses->disconnect;
2871 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2872 $self->push_events($resv);
2878 # returns true if the item was used (or may potentially be used
2879 # in subsequent calls) to capture a hold.
2880 sub attempt_checkin_hold_capture {
2882 my $copy = $self->copy;
2884 # we've been explicitly told not to capture any holds
2885 return 0 if $self->capture eq 'nocapture';
2887 # See if this copy can fulfill any holds
2888 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2889 $self->editor, $copy, $self->editor->requestor );
2892 $logger->debug("circulator: no potential permitted".
2893 "holds found for copy ".$copy->barcode);
2897 if($self->capture ne 'capture') {
2898 # see if this item is in a hold-capture-delay location
2899 my $location = $self->copy->location;
2900 if(!ref($location)) {
2901 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2902 $self->copy->location($location);
2904 if($U->is_true($location->hold_verify)) {
2905 $self->bail_on_events(
2906 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2911 $self->retarget($retarget);
2913 my $suppress_transit = 0;
2914 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2915 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2916 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2917 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2918 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2919 $suppress_transit = 1;
2920 $hold->pickup_lib($self->circ_lib);
2925 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2927 $hold->current_copy($copy->id);
2928 $hold->capture_time('now');
2929 $self->put_hold_on_shelf($hold)
2930 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2932 # prevent DB errors caused by fetching
2933 # holds from storage, and updating through cstore
2934 $hold->clear_fulfillment_time;
2935 $hold->clear_fulfillment_staff;
2936 $hold->clear_fulfillment_lib;
2937 $hold->clear_expire_time;
2938 $hold->clear_cancel_time;
2939 $hold->clear_prev_check_time unless $hold->prev_check_time;
2941 $self->bail_on_events($self->editor->event)
2942 unless $self->editor->update_action_hold_request($hold);
2944 $self->checkin_changed(1);
2946 return 0 if $self->bail_out;
2948 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2950 if ($hold->hold_type eq 'R') {
2951 $copy->status(OILS_COPY_STATUS_CATALOGING);
2952 $hold->fulfillment_time('now');
2953 $self->noop(1); # Block other transit/hold checks
2954 $self->bail_on_events($self->editor->event)
2955 unless $self->editor->update_action_hold_request($hold);
2957 # This hold was captured in the correct location
2958 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2959 $self->push_events(OpenILS::Event->new('SUCCESS'));
2961 #$self->do_hold_notify($hold->id);
2962 $self->notify_hold($hold->id);
2967 # Hold needs to be picked up elsewhere. Build a hold
2968 # transit and route the item.
2969 $self->checkin_build_hold_transit();
2970 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2971 return 0 if $self->bail_out;
2972 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2975 # make sure we save the copy status
2977 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
2981 sub attempt_checkin_reservation_capture {
2983 my $copy = $self->copy;
2985 # we've been explicitly told not to capture any holds
2986 return 0 if $self->capture eq 'nocapture';
2988 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2989 my $evt = $booking_ses->request(
2990 "open-ils.booking.resources.capture_for_reservation",
2991 $self->editor->authtoken,
2993 1 # don't update copy - we probably have it locked
2995 $booking_ses->disconnect;
2997 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2999 "open-ils.booking.resources.capture_for_reservation " .
3000 "didn't return an event!"
3004 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3005 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3007 # not-transferable is an error event we'll pass on the user
3008 $logger->warn("reservation capture attempted against non-transferable item");
3009 $self->push_events($evt);
3011 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3012 # Re-retrieve copy as reservation capture may have changed
3013 # its status and whatnot.
3015 "circulator: booking capture win on copy " . $self->copy->id
3017 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3019 "circulator: changing copy " . $self->copy->id .
3020 "'s status from " . $self->copy->status . " to " .
3023 $self->copy->status($new_copy_status);
3026 $self->reservation($evt->{"payload"}->{"reservation"});
3028 if (exists $evt->{"payload"}->{"transit"}) {
3032 "org" => $evt->{"payload"}->{"transit"}->dest
3036 $self->checkin_changed(1);
3040 # other results are treated as "nothing to capture"
3044 sub do_hold_notify {
3045 my( $self, $holdid ) = @_;
3047 my $e = new_editor(xact => 1);
3048 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3050 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3051 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3053 $logger->info("circulator: running delayed hold notify process");
3055 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3056 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3058 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3059 hold_id => $holdid, requestor => $self->editor->requestor);
3061 $logger->debug("circulator: built hold notifier");
3063 if(!$notifier->event) {
3065 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3067 my $stat = $notifier->send_email_notify;
3068 if( $stat == '1' ) {
3069 $logger->info("circulator: hold notify succeeded for hold $holdid");
3073 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3076 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3080 sub retarget_holds {
3082 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3083 my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
3084 $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
3085 # no reason to wait for the return value
3089 sub checkin_build_hold_transit {
3092 my $copy = $self->copy;
3093 my $hold = $self->hold;
3094 my $trans = Fieldmapper::action::hold_transit_copy->new;
3096 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3098 $trans->hold($hold->id);
3099 $trans->source($self->circ_lib);
3100 $trans->dest($hold->pickup_lib);
3101 $trans->source_send_time("now");
3102 $trans->target_copy($copy->id);
3104 # when the copy gets to its destination, it will recover
3105 # this status - put it onto the holds shelf
3106 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3108 return $self->bail_on_events($self->editor->event)
3109 unless $self->editor->create_action_hold_transit_copy($trans);
3114 sub process_received_transit {
3116 my $copy = $self->copy;
3117 my $copyid = $self->copy->id;
3119 my $status_name = $U->copy_status($copy->status)->name;
3120 $logger->debug("circulator: attempting transit receive on ".
3121 "copy $copyid. Copy status is $status_name");
3123 my $transit = $self->transit;
3125 # Check if we are in a transit suppress range
3126 my $suppress_transit = 0;
3127 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3128 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3129 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3130 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3131 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3132 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3133 $suppress_transit = 1;
3134 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3138 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3139 # - this item is in-transit to a different location
3140 # - Or we are capturing holds as transits, so why create a new transit?
3142 my $tid = $transit->id;
3143 my $loc = $self->circ_lib;
3144 my $dest = $transit->dest;
3146 $logger->info("circulator: Fowarding transit on copy which is destined ".
3147 "for a different location. transit=$tid, copy=$copyid, current ".
3148 "location=$loc, destination location=$dest");
3150 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3152 # grab the associated hold object if available
3153 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3154 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3156 return $self->bail_on_events($evt);
3159 # The transit is received, set the receive time
3160 $transit->dest_recv_time('now');
3161 $self->bail_on_events($self->editor->event)
3162 unless $self->editor->update_action_transit_copy($transit);
3164 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3166 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3167 $copy->status( $transit->copy_status );
3168 $self->update_copy();
3169 return if $self->bail_out;
3173 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3176 # hold has arrived at destination, set shelf time
3177 $self->put_hold_on_shelf($hold);
3178 $self->bail_on_events($self->editor->event)
3179 unless $self->editor->update_action_hold_request($hold);
3180 return if $self->bail_out;
3182 $self->notify_hold($hold_transit->hold);
3185 $hold_transit = undef;
3186 $self->cancelled_hold_transit(1);
3187 $self->reshelve_copy(1);
3188 $self->fake_hold_dest(0);
3193 OpenILS::Event->new(
3196 payload => { transit => $transit, holdtransit => $hold_transit } ));
3198 return $hold_transit;
3202 # ------------------------------------------------------------------
3203 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3204 # ------------------------------------------------------------------
3205 sub put_hold_on_shelf {
3206 my($self, $hold) = @_;
3207 $hold->shelf_time('now');
3208 $hold->current_shelf_lib($self->circ_lib);
3209 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3215 my $reservation = shift;
3216 my $dt_parser = DateTime::Format::ISO8601->new;
3218 my $obj = $reservation ? $self->reservation : $self->circ;
3220 my $lost_bill_opts = $self->lost_bill_options;
3221 my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3222 # first, restore any voided overdues for lost, if needed
3223 if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3224 my $restore_od = $U->ou_ancestor_setting_value(
3225 $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3226 $self->editor) || 0;
3227 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3231 # next, handle normal overdue generation and apply stop_fines
3232 # XXX reservations don't have stop_fines
3233 # TODO revisit booking_reservation re: stop_fines support
3234 if ($reservation or !$obj->stop_fines) {
3237 # This is a crude check for whether we are in a grace period. The code
3238 # in generate_fines() does a more thorough job, so this exists solely
3239 # as a small optimization, and might be better off removed.
3241 # If we have a grace period
3242 if($obj->can('grace_period')) {
3243 # Parse out the due date
3244 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3245 # Add the grace period to the due date
3246 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3247 # Don't generate fines on circs still in grace period
3248 $skip_for_grace = $due_date > DateTime->now;
3250 $CC->generate_fines({circs => [$obj], editor => $self->editor})
3251 unless $skip_for_grace;
3253 if (!$reservation and !$obj->stop_fines) {
3254 $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3255 $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3256 $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3257 $obj->stop_fines_time('now');
3258 $obj->stop_fines_time($self->backdate) if $self->backdate;
3259 $self->editor->update_action_circulation($obj);
3263 # finally, handle voiding of lost item and processing fees
3264 if ($self->needs_lost_bill_handling) {
3265 my $void_cost = $U->ou_ancestor_setting_value(
3266 $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3267 $self->editor) || 0;
3268 my $void_proc_fee = $U->ou_ancestor_setting_value(
3269 $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3270 $self->editor) || 0;
3271 $self->checkin_handle_lost_or_lo_now_found(
3272 $lost_bill_opts->{void_cost_btype},
3273 $lost_bill_opts->{is_longoverdue}) if $void_cost;
3274 $self->checkin_handle_lost_or_lo_now_found(
3275 $lost_bill_opts->{void_fee_btype},
3276 $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3282 sub checkin_handle_circ_start {
3284 my $circ = $self->circ;
3285 my $copy = $self->copy;
3289 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3291 # backdate the circ if necessary
3292 if($self->backdate) {
3293 my $evt = $self->checkin_handle_backdate;
3294 return $self->bail_on_events($evt) if $evt;
3297 # Set the checkin vars since we have the item
3298 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3300 # capture the true scan time for back-dated checkins
3301 $circ->checkin_scan_time('now');
3303 $circ->checkin_staff($self->editor->requestor->id);
3304 $circ->checkin_lib($self->circ_lib);
3305 $circ->checkin_workstation($self->editor->requestor->wsid);
3307 my $circ_lib = (ref $self->copy->circ_lib) ?
3308 $self->copy->circ_lib->id : $self->copy->circ_lib;
3309 my $stat = $U->copy_status($self->copy->status)->id;
3311 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3312 # we will now handle lost fines, but the copy will retain its 'lost'
3313 # status if it needs to transit home unless lost_immediately_available
3316 # if we decide to also delay fine handling until the item arrives home,
3317 # we will need to call lost fine handling code both when checking items
3318 # in and also when receiving transits
3319 $self->checkin_handle_lost($circ_lib);
3320 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3321 # same process as above.
3322 $self->checkin_handle_long_overdue($circ_lib);
3323 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3324 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3326 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3333 sub checkin_handle_circ_finish {
3335 my $e = $self->editor;
3336 my $circ = $self->circ;
3338 # Do one last check before the final circulation update to see
3339 # if the xact_finish value should be set or not.
3341 # The underlying money.billable_xact may have been updated to
3342 # reflect a change in xact_finish during checkin bills handling,
3343 # however we can't simply refresh the circulation from the DB,
3344 # because other changes may be pending. Instead, reproduce the
3345 # xact_finish check here. It won't hurt to do it again.
3347 my $sum = $e->retrieve_money_billable_transaction_summary($circ->id);
3348 if ($sum) { # is this test still needed?
3350 my $balance = $sum->balance_owed;
3352 if ($balance == 0) {
3353 $circ->xact_finish('now');
3355 $circ->clear_xact_finish;
3358 $logger->info("circulator: $balance is owed on this circulation");
3361 return $self->bail_on_events($e->event)
3362 unless $e->update_action_circulation($circ);
3367 # ------------------------------------------------------------------
3368 # See if we need to void billings, etc. for lost checkin
3369 # ------------------------------------------------------------------
3370 sub checkin_handle_lost {
3372 my $circ_lib = shift;
3374 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3375 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3377 $self->lost_bill_options({
3378 circ_lib => $circ_lib,
3379 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3380 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3381 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3382 void_cost_btype => 3,
3386 return $self->checkin_handle_lost_or_longoverdue(
3387 circ_lib => $circ_lib,
3388 max_return => $max_return,
3389 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3390 ous_use_last_activity => undef # not supported for LOST checkin
3394 # ------------------------------------------------------------------
3395 # See if we need to void billings, etc. for long-overdue checkin
3396 # note: not using constants below since they serve little purpose
3397 # for single-use strings that are descriptive in their own right
3398 # and mostly just complicate debugging.
3399 # ------------------------------------------------------------------
3400 sub checkin_handle_long_overdue {
3402 my $circ_lib = shift;
3404 $logger->info("circulator: processing long-overdue checkin...");
3406 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3407 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3409 $self->lost_bill_options({
3410 circ_lib => $circ_lib,
3411 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3412 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3413 is_longoverdue => 1,
3414 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3415 void_cost_btype => 10,
3416 void_fee_btype => 11
3419 return $self->checkin_handle_lost_or_longoverdue(
3420 circ_lib => $circ_lib,
3421 max_return => $max_return,
3422 ous_immediately_available => 'circ.longoverdue_immediately_available',
3423 ous_use_last_activity =>
3424 'circ.longoverdue.use_last_activity_date_on_return'
3428 # last billing activity is last payment time, last billing time, or the
3429 # circ due date. If the relevant "use last activity" org unit setting is
3430 # false/unset, then last billing activity is always the due date.
3431 sub get_circ_last_billing_activity {
3433 my $circ_lib = shift;
3434 my $setting = shift;
3435 my $date = $self->circ->due_date;
3437 return $date unless $setting and
3438 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3440 my $xact = $self->editor->retrieve_money_billable_transaction([
3442 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3445 if ($xact->summary) {
3446 $date = $xact->summary->last_payment_ts ||
3447 $xact->summary->last_billing_ts ||
3448 $self->circ->due_date;
3455 sub checkin_handle_lost_or_longoverdue {
3456 my ($self, %args) = @_;
3458 my $circ = $self->circ;
3459 my $max_return = $args{max_return};
3460 my $circ_lib = $args{circ_lib};
3465 $self->get_circ_last_billing_activity(
3466 $circ_lib, $args{ous_use_last_activity});
3469 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3470 $tm[5] -= 1 if $tm[5] > 0;
3471 my $due = timelocal(int($tm[1]), int($tm[2]),
3472 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3475 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3477 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3478 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3479 "DUE: $due LAST: $last_chance");
3481 $max_return = 0 if $today < $last_chance;
3487 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3488 "return interval. skipping fine/fee voiding, etc.");
3490 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3492 $logger->info("circulator: check-in of lost/lo item having a balance ".
3493 "of zero, skipping fine/fee voiding and reinstatement.");
3495 } else { # within max-return interval or no interval defined
3497 $logger->info("circulator: check-in of lost/lo item is within the ".
3498 "max return interval (or no interval is defined). Proceeding ".
3499 "with fine/fee voiding, etc.");
3501 $self->needs_lost_bill_handling(1);
3504 if ($circ_lib != $self->circ_lib) {
3505 # if the item is not home, check to see if we want to retain the
3506 # lost/longoverdue status at this point in the process
3508 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3509 $args{ous_immediately_available}, $self->editor) || 0;
3511 if ($immediately_available) {
3512 # item status does not need to be retained, so give it a
3513 # reshelving status as if it were a normal checkin
3514 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3517 $logger->info("circulator: leaving lost/longoverdue copy".
3518 " status in place on checkin");
3521 # lost/longoverdue item is home and processed, treat like a normal
3522 # checkin from this point on
3523 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3529 sub checkin_handle_backdate {
3532 # ------------------------------------------------------------------
3533 # clean up the backdate for date comparison
3534 # XXX We are currently taking the due-time from the original due-date,
3535 # not the input. Do we need to do this? This certainly interferes with
3536 # backdating of hourly checkouts, but that is likely a very rare case.
3537 # ------------------------------------------------------------------
3538 my $bd = cleanse_ISO8601($self->backdate);
3539 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3540 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3541 $new_date->set_hour($original_date->hour());
3542 $new_date->set_minute($original_date->minute());
3543 if ($new_date >= DateTime->now) {
3544 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3545 # $self->backdate() autoload handler ignores undef values.
3546 # Clear the backdate manually.
3547 $logger->info("circulator: ignoring future backdate: $new_date");
3548 delete $self->{backdate};
3550 $self->backdate(cleanse_ISO8601($new_date->datetime()));
3557 sub check_checkin_copy_status {
3559 my $copy = $self->copy;
3561 my $status = $U->copy_status($copy->status)->id;
3564 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3565 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3566 $status == OILS_COPY_STATUS_IN_PROCESS ||
3567 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3568 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3569 $status == OILS_COPY_STATUS_CATALOGING ||
3570 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3571 $status == OILS_COPY_STATUS_CANCELED_TRANSIT ||
3572 $status == OILS_COPY_STATUS_RESHELVING );
3574 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3575 if( $status == OILS_COPY_STATUS_LOST );
3577 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3578 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3580 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3581 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3583 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3584 if( $status == OILS_COPY_STATUS_MISSING );
3586 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3591 # --------------------------------------------------------------------------
3592 # On checkin, we need to return as many relevant objects as we can
3593 # --------------------------------------------------------------------------
3594 sub checkin_flesh_events {
3597 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3598 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3599 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3602 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3605 if($self->hold and !$self->hold->cancel_time) {
3606 $hold = $self->hold;
3607 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3611 # update our copy of the circ object and
3612 # flesh the billing summary data
3614 $self->editor->retrieve_action_circulation([
3618 circ => ['billable_transaction'],
3627 # flesh some patron fields before returning
3629 $self->editor->retrieve_actor_user([
3634 au => ['card', 'billing_address', 'mailing_address']
3641 for my $evt (@{$self->events}) {
3644 $payload->{copy} = $U->unflesh_copy($self->copy);
3645 $payload->{volume} = $self->volume;
3646 $payload->{record} = $record,
3647 $payload->{circ} = $self->circ;
3648 $payload->{transit} = $self->transit;
3649 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3650 $payload->{hold} = $hold;
3651 $payload->{patron} = $self->patron;
3652 $payload->{reservation} = $self->reservation
3653 unless (not $self->reservation or $self->reservation->cancel_time);
3655 $evt->{payload} = $payload;
3660 my( $self, $msg ) = @_;
3661 my $bc = ($self->copy) ? $self->copy->barcode :
3664 my $usr = ($self->patron) ? $self->patron->id : "";
3665 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3666 ", recipient=$usr, copy=$bc");
3672 $self->log_me("do_renew()");
3674 # Make sure there is an open circ to renew
3675 my $usrid = $self->patron->id if $self->patron;
3676 my $circ = $self->editor->search_action_circulation({
3677 target_copy => $self->copy->id,
3678 xact_finish => undef,
3679 checkin_time => undef,
3680 ($usrid ? (usr => $usrid) : ())
3683 return $self->bail_on_events($self->editor->event) unless $circ;
3685 # A user is not allowed to renew another user's items without permission
3686 unless( $circ->usr eq $self->editor->requestor->id ) {
3687 return $self->bail_on_events($self->editor->events)
3688 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3691 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3692 if $circ->renewal_remaining < 1;
3694 # -----------------------------------------------------------------
3696 $self->parent_circ($circ->id);
3697 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3700 # Opac renewal - re-use circ library from original circ (unless told not to)
3701 if($self->opac_renewal) {
3702 unless(defined($opac_renewal_use_circ_lib)) {
3703 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3704 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3705 $opac_renewal_use_circ_lib = 1;
3708 $opac_renewal_use_circ_lib = 0;
3711 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3714 # Desk renewal - re-use circ library from original circ (unless told not to)
3715 if($self->desk_renewal) {
3716 unless(defined($desk_renewal_use_circ_lib)) {
3717 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3718 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3719 $desk_renewal_use_circ_lib = 1;
3722 $desk_renewal_use_circ_lib = 0;
3725 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3728 # Run the fine generator against the old circ
3729 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3730 # a few lines down. Commenting out, for now.
3731 #$self->handle_fines;
3733 $self->run_renew_permit;
3736 $self->do_checkin();
3737 return if $self->bail_out;
3739 unless( $self->permit_override ) {
3741 return if $self->bail_out;
3742 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3743 $self->remove_event('ITEM_NOT_CATALOGED');
3746 $self->override_events;
3747 return if $self->bail_out;
3750 $self->do_checkout();
3755 my( $self, $evt ) = @_;
3756 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3757 $logger->debug("circulator: removing event from list: $evt");
3758 my @events = @{$self->events};
3759 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3764 my( $self, $evt ) = @_;
3765 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3766 return grep { $_->{textcode} eq $evt } @{$self->events};
3770 sub run_renew_permit {
3773 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3774 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3775 $self->editor, $self->copy, $self->editor->requestor, 1
3777 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3780 my $results = $self->run_indb_circ_test;
3781 $self->push_events($self->matrix_test_result_events)
3782 unless $self->circ_test_success;
3786 # XXX: The primary mechanism for storing circ history is now handled
3787 # by tracking real circulation objects instead of bibs in a bucket.
3788 # However, this code is disabled by default and could be useful
3789 # some day, so may as well leave it for now.
3790 sub append_reading_list {
3794 $self->is_checkout and
3800 # verify history is globally enabled and uses the bucket mechanism
3801 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3802 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3804 return undef unless $htype and $htype eq 'bucket';
3806 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3808 # verify the patron wants to retain the hisory
3809 my $setting = $e->search_actor_user_setting(
3810 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3812 unless($setting and $setting->value) {
3817 my $bkt = $e->search_container_copy_bucket(
3818 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3823 # find the next item position
3824 my $last_item = $e->search_container_copy_bucket_item(
3825 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3826 $pos = $last_item->pos + 1 if $last_item;
3829 # create the history bucket if necessary
3830 $bkt = Fieldmapper::container::copy_bucket->new;
3831 $bkt->owner($self->patron->id);
3833 $bkt->btype('circ_history');
3835 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3838 my $item = Fieldmapper::container::copy_bucket_item->new;
3840 $item->bucket($bkt->id);
3841 $item->target_copy($self->copy->id);
3844 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3851 sub make_trigger_events {
3853 return unless $self->circ;
3854 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3855 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3856 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3861 sub checkin_handle_lost_or_lo_now_found {
3862 my ($self, $bill_type, $is_longoverdue) = @_;
3864 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3866 $logger->debug("voiding $tag item billings");
3867 my $result = $CC->void_or_zero_bills_of_type($self->editor, $self->circ, $self->copy, $bill_type, "$tag ITEM RETURNED");
3868 $self->bail_on_events($self->editor->event) if ($result);
3871 sub checkin_handle_lost_or_lo_now_found_restore_od {
3873 my $circ_lib = shift;
3874 my $is_longoverdue = shift;
3875 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3877 # ------------------------------------------------------------------
3878 # restore those overdue charges voided when item was set to lost
3879 # ------------------------------------------------------------------
3881 my $ods = $self->editor->search_money_billing([
3883 xact => $self->circ->id,
3887 order_by => {mb => 'billing_ts desc'}
3891 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
3892 # Because actual users get up to all kinds of unexpectedness, we
3893 # only recreate up to $circ->max_fine in bills. I know you think
3894 # it wouldn't happen that bills could get created, voided, and
3895 # recreated more than once, but I guaran-damn-tee you that it will
3897 if ($ods && @$ods) {
3898 my $void_amount = 0;
3899 my $void_max = $self->circ->max_fine();
3900 # search for overdues voided the new way (aka "adjusted")
3901 my @billings = map {$_->id()} @$ods;
3902 my $voids = $self->editor->search_money_account_adjustment(
3904 billing => \@billings
3908 map {$void_amount += $_->amount()} @$voids;
3910 # if no adjustments found, assume they were voided the old way (aka "voided")
3911 for my $bill (@$ods) {
3912 if( $U->is_true($bill->voided) ) {
3913 $void_amount += $bill->amount();
3919 ($void_amount < $void_max ? $void_amount : $void_max),
3921 $ods->[0]->billing_type(),
3923 "System: $tag RETURNED - OVERDUES REINSTATED",
3924 $ods->[0]->billing_ts() # date this restoration the same as the last overdue (for possible subsequent fine generation)