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 # checkin and reservation return can result in modifications to
294 # actor.usr.claims_never_checked_out_count without also modifying
295 # actor.last_xact_id. Perform a no-op update on the patron to
296 # force an update to last_xact_id.
297 if ($circulator->claims_never_checked_out && $circulator->patron) {
298 $circulator->editor->update_actor_user(
299 $circulator->editor->retrieve_actor_user($circulator->patron->id))
300 or return $circulator->editor->die_event;
303 $circulator->editor->commit;
306 $conn->respond_complete(circ_events($circulator));
308 return undef if $circulator->bail_out;
310 $circulator->do_hold_notify($circulator->notify_hold)
311 if $circulator->notify_hold;
312 $circulator->retarget_holds if $circulator->retarget;
313 $circulator->append_reading_list;
314 $circulator->make_trigger_events;
321 my @e = @{$circ->events};
322 # if we have multiple events, SUCCESS should not be one of them;
323 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
324 return (@e == 1) ? $e[0] : \@e;
328 sub translate_legacy_args {
331 if( $$args{barcode} ) {
332 $$args{copy_barcode} = $$args{barcode};
333 delete $$args{barcode};
336 if( $$args{copyid} ) {
337 $$args{copy_id} = $$args{copyid};
338 delete $$args{copyid};
341 if( $$args{patronid} ) {
342 $$args{patron_id} = $$args{patronid};
343 delete $$args{patronid};
346 if( $$args{patron} and !ref($$args{patron}) ) {
347 $$args{patron_id} = $$args{patron};
348 delete $$args{patron};
352 if( $$args{noncat} ) {
353 $$args{is_noncat} = $$args{noncat};
354 delete $$args{noncat};
357 if( $$args{precat} ) {
358 $$args{is_precat} = $$args{request_precat} = $$args{precat};
359 delete $$args{precat};
365 # --------------------------------------------------------------------------
366 # This package actually manages all of the circulation logic
367 # --------------------------------------------------------------------------
368 package OpenILS::Application::Circ::Circulator;
369 use strict; use warnings;
370 use vars q/$AUTOLOAD/;
372 use OpenILS::Utils::Fieldmapper;
373 use OpenSRF::Utils::Cache;
374 use Digest::MD5 qw(md5_hex);
375 use DateTime::Format::ISO8601;
376 use OpenILS::Utils::PermitHold;
377 use OpenSRF::Utils qw/:datetime/;
378 use OpenSRF::Utils::SettingsClient;
379 use OpenILS::Application::Circ::Holds;
380 use OpenILS::Application::Circ::Transit;
381 use OpenSRF::Utils::Logger qw(:logger);
382 use OpenILS::Utils::CStoreEditor qw/:funcs/;
383 use OpenILS::Const qw/:const/;
384 use OpenILS::Utils::Penalty;
385 use OpenILS::Application::Circ::CircCommon;
388 my $CC = "OpenILS::Application::Circ::CircCommon";
389 my $holdcode = "OpenILS::Application::Circ::Holds";
390 my $transcode = "OpenILS::Application::Circ::Transit";
396 # --------------------------------------------------------------------------
397 # Add a pile of automagic getter/setter methods
398 # --------------------------------------------------------------------------
399 my @AUTOLOAD_FIELDS = qw/
445 recurring_fines_level
458 cancelled_hold_transit
465 circ_matrix_matchpoint
476 claims_never_checked_out
489 dont_change_lost_zero
491 needs_lost_bill_handling
497 my $type = ref($self) or die "$self is not an object";
499 my $name = $AUTOLOAD;
502 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
503 $logger->error("circulator: $type: invalid autoload field: $name");
504 die "$type: invalid autoload field: $name\n"
509 *{"${type}::${name}"} = sub {
512 $s->{$name} = $v if defined $v;
516 return $self->$name($data);
521 my( $class, $auth, %args ) = @_;
522 $class = ref($class) || $class;
523 my $self = bless( {}, $class );
526 $self->editor(new_editor(xact => 1, authtoken => $auth));
528 unless( $self->editor->checkauth ) {
529 $self->bail_on_events($self->editor->event);
533 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
535 $self->$_($args{$_}) for keys %args;
538 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
540 # if this is a renewal, default to desk_renewal
541 $self->desk_renewal(1) unless
542 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
544 $self->capture('') unless $self->capture;
546 unless(%user_groups) {
547 my $gps = $self->editor->retrieve_all_permission_grp_tree;
548 %user_groups = map { $_->id => $_ } @$gps;
555 # --------------------------------------------------------------------------
556 # True if we should discontinue processing
557 # --------------------------------------------------------------------------
559 my( $self, $bool ) = @_;
560 if( defined $bool ) {
561 $logger->info("circulator: BAILING OUT") if $bool;
562 $self->{bail_out} = $bool;
564 return $self->{bail_out};
569 my( $self, @evts ) = @_;
572 $e->{payload} = $self->copy if
573 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
575 $logger->info("circulator: pushing event ".$e->{textcode});
576 push( @{$self->events}, $e ) unless
577 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
583 return '' if $self->skip_permit_key;
584 my $key = md5_hex( time() . rand() . "$$" );
585 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
586 return $self->permit_key($key);
589 sub check_permit_key {
591 return 1 if $self->skip_permit_key;
592 my $key = $self->permit_key;
593 return 0 unless $key;
594 my $k = "oils_permit_key_$key";
595 my $one = $self->cache_handle->get_cache($k);
596 $self->cache_handle->delete_cache($k);
597 return ($one) ? 1 : 0;
600 sub seems_like_reservation {
603 # Some words about the following method:
604 # 1) It requires the VIEW_USER permission, but that's not an
605 # issue, right, since all staff should have that?
606 # 2) It returns only one reservation at a time, even if an item can be
607 # and is currently overbooked. Hmmm....
608 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
609 my $result = $booking_ses->request(
610 "open-ils.booking.reservations.by_returnable_resource_barcode",
611 $self->editor->authtoken,
614 $booking_ses->disconnect;
616 return $self->bail_on_events($result) if defined $U->event_code($result);
619 $self->reservation(shift @$result);
627 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
628 sub save_trimmed_copy {
629 my ($self, $copy) = @_;
632 $self->volume($copy->call_number);
633 $self->title($self->volume->record);
634 $self->copy->call_number($self->volume->id);
635 $self->volume->record($self->title->id);
636 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
637 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
638 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
639 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
645 my $e = $self->editor;
647 # --------------------------------------------------------------------------
648 # Grab the fleshed copy
649 # --------------------------------------------------------------------------
650 unless($self->is_noncat) {
653 $copy = $e->retrieve_asset_copy(
654 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
656 } elsif( $self->copy_barcode ) {
658 $copy = $e->search_asset_copy(
659 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
660 } elsif( $self->reservation ) {
661 my $res = $e->json_query(
663 "select" => {"acp" => ["id"]},
668 "field" => "barcode",
672 "field" => "current_resource"
680 "id" => (ref $self->reservation) ?
681 $self->reservation->id : $self->reservation
686 if (ref $res eq "ARRAY" and scalar @$res) {
687 $logger->info("circulator: mapped reservation " .
688 $self->reservation . " to copy " . $res->[0]->{"id"});
689 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
694 $self->save_trimmed_copy($copy);
696 # We can't renew if there is no copy
697 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
698 if $self->is_renewal;
703 # --------------------------------------------------------------------------
705 # --------------------------------------------------------------------------
709 flesh_fields => {au => [ qw/ card / ]}
712 if( $self->patron_id ) {
713 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
714 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
716 } elsif( $self->patron_barcode ) {
718 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
719 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
720 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
722 $patron = $e->retrieve_actor_user($card->usr)
723 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
725 # Use the card we looked up, not the patron's primary, for card active checks
726 $patron->card($card);
729 if( my $copy = $self->copy ) {
732 $flesh->{flesh_fields}->{circ} = ['usr'];
734 my $circ = $e->search_action_circulation([
735 {target_copy => $copy->id, checkin_time => undef}, $flesh
739 $patron = $circ->usr;
740 $circ->usr($patron->id); # de-flesh for consistency
746 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
747 unless $self->patron($patron) or $self->is_checkin;
749 unless($self->is_checkin) {
751 # Check for inactivity and patron reg. expiration
753 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
754 unless $U->is_true($patron->active);
756 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
757 unless $U->is_true($patron->card->active);
759 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
760 cleanse_ISO8601($patron->expire_date));
762 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
763 if( CORE::time > $expire->epoch ) ;
768 # --------------------------------------------------------------------------
769 # Does the circ permit work
770 # --------------------------------------------------------------------------
774 $self->log_me("do_permit()");
776 unless( $self->editor->requestor->id == $self->patron->id ) {
777 return $self->bail_on_events($self->editor->event)
778 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
781 $self->check_captured_holds();
782 $self->do_copy_checks();
783 return if $self->bail_out;
784 $self->run_patron_permit_scripts();
785 $self->run_copy_permit_scripts()
786 unless $self->is_precat or $self->is_noncat;
787 $self->check_item_deposit_events();
788 $self->override_events();
789 return if $self->bail_out;
791 if($self->is_precat and not $self->request_precat) {
794 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
795 return $self->bail_out(1) unless $self->is_renewal;
799 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
802 sub check_item_deposit_events {
804 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
805 if $self->is_deposit and not $self->is_deposit_exempt;
806 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
807 if $self->is_rental and not $self->is_rental_exempt;
810 # returns true if the user is not required to pay deposits
811 sub is_deposit_exempt {
813 my $pid = (ref $self->patron->profile) ?
814 $self->patron->profile->id : $self->patron->profile;
815 my $groups = $U->ou_ancestor_setting_value(
816 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
817 for my $grp (@$groups) {
818 return 1 if $self->is_group_descendant($grp, $pid);
823 # returns true if the user is not required to pay rental fees
824 sub is_rental_exempt {
826 my $pid = (ref $self->patron->profile) ?
827 $self->patron->profile->id : $self->patron->profile;
828 my $groups = $U->ou_ancestor_setting_value(
829 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
830 for my $grp (@$groups) {
831 return 1 if $self->is_group_descendant($grp, $pid);
836 sub is_group_descendant {
837 my($self, $p_id, $c_id) = @_;
838 return 0 unless defined $p_id and defined $c_id;
839 return 1 if $c_id == $p_id;
840 while(my $grp = $user_groups{$c_id}) {
841 $c_id = $grp->parent;
842 return 0 unless defined $c_id;
843 return 1 if $c_id == $p_id;
848 sub check_captured_holds {
850 my $copy = $self->copy;
851 my $patron = $self->patron;
853 return undef unless $copy;
855 my $s = $U->copy_status($copy->status)->id;
856 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
857 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
859 # Item is on the holds shelf, make sure it's going to the right person
860 my $hold = $self->editor->search_action_hold_request(
863 current_copy => $copy->id ,
864 capture_time => { '!=' => undef },
865 cancel_time => undef,
866 fulfillment_time => undef
872 if ($hold and $hold->usr == $patron->id) {
873 $self->checkout_is_for_hold(1);
877 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
879 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
885 my $copy = $self->copy;
888 my $stat = $U->copy_status($copy->status)->id;
890 # We cannot check out a copy if it is in-transit
891 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
892 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
895 $self->handle_claims_returned();
896 return if $self->bail_out;
898 # no claims returned circ was found, check if there is any open circ
899 unless( $self->is_renewal ) {
901 my $circs = $self->editor->search_action_circulation(
902 { target_copy => $copy->id, checkin_time => undef }
905 if(my $old_circ = $circs->[0]) { # an open circ was found
907 my $payload = {copy => $copy};
909 if($old_circ->usr == $self->patron->id) {
911 $payload->{old_circ} = $old_circ;
913 # If there is an open circulation on the checkout item and an auto-renew
914 # interval is defined, inform the caller that they should go
915 # ahead and renew the item instead of warning about open circulations.
917 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
919 'circ.checkout_auto_renew_age',
923 if($auto_renew_intvl) {
924 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
925 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
927 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
928 $payload->{auto_renew} = 1;
933 return $self->bail_on_events(
934 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
940 my $LEGACY_CIRC_EVENT_MAP = {
941 'no_item' => 'ITEM_NOT_CATALOGED',
942 'actor.usr.barred' => 'PATRON_BARRED',
943 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
944 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
945 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
946 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
947 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
948 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
949 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
950 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
951 'config.circ_matrix_test.total_copy_hold_ratio' =>
952 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
953 'config.circ_matrix_test.available_copy_hold_ratio' =>
954 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
958 # ---------------------------------------------------------------------
959 # This pushes any patron-related events into the list but does not
960 # set bail_out for any events
961 # ---------------------------------------------------------------------
962 sub run_patron_permit_scripts {
964 my $patronid = $self->patron->id;
969 my $results = $self->run_indb_circ_test;
970 unless($self->circ_test_success) {
973 if ($self->is_noncat) {
974 # no_item result is OK during noncat checkout
975 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
979 if ($self->checkout_is_for_hold) {
980 # if this checkout will fulfill a hold, ignore CIRC blocks
981 # and rely instead on the (later-checked) FULFILL block
983 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
984 my $fblock_pens = $self->editor->search_config_standing_penalty(
985 {name => [@pen_names], block_list => {like => '%CIRC%'}});
987 for my $res (@$results) {
988 my $name = $res->{fail_part} || '';
989 next if grep {$_->name eq $name} @$fblock_pens;
990 push(@trimmed_results, $res);
994 # not for hold or noncat
995 @trimmed_results = @$results;
999 # update the final set of test results
1000 $self->matrix_test_result(\@trimmed_results);
1002 push @allevents, $self->matrix_test_result_events;
1006 $_->{payload} = $self->copy if
1007 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1010 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1012 $self->push_events(@allevents);
1015 sub matrix_test_result_codes {
1017 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1020 sub matrix_test_result_events {
1023 my $event = new OpenILS::Event(
1024 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1026 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1028 } (@{$self->matrix_test_result});
1031 sub run_indb_circ_test {
1033 return $self->matrix_test_result if $self->matrix_test_result;
1035 my $dbfunc = ($self->is_renewal) ?
1036 'action.item_user_renew_test' : 'action.item_user_circ_test';
1038 if( $self->is_precat && $self->request_precat) {
1039 $self->make_precat_copy;
1040 return if $self->bail_out;
1043 my $results = $self->editor->json_query(
1047 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1053 $self->circ_test_success($U->is_true($results->[0]->{success}));
1055 if(my $mp = $results->[0]->{matchpoint}) {
1056 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1057 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1058 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1059 if(defined($results->[0]->{renewals})) {
1060 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1062 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1063 if(defined($results->[0]->{grace_period})) {
1064 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1066 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1067 if(defined($results->[0]->{hard_due_date})) {
1068 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1070 # Grab the *last* response for limit_groups, where it is more likely to be filled
1071 $self->limit_groups($results->[-1]->{limit_groups});
1074 return $self->matrix_test_result($results);
1077 # ---------------------------------------------------------------------
1078 # given a use and copy, this will calculate the circulation policy
1079 # parameters. Only works with in-db circ.
1080 # ---------------------------------------------------------------------
1084 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1086 $self->run_indb_circ_test;
1089 circ_test_success => $self->circ_test_success,
1090 failure_events => [],
1091 failure_codes => [],
1092 matchpoint => $self->circ_matrix_matchpoint
1095 unless($self->circ_test_success) {
1096 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1097 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1100 if($self->circ_matrix_matchpoint) {
1101 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1102 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1103 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1104 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1106 my $policy = $self->get_circ_policy(
1107 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1109 $$results{$_} = $$policy{$_} for keys %$policy;
1115 # ---------------------------------------------------------------------
1116 # Loads the circ policy info for duration, recurring fine, and max
1117 # fine based on the current copy
1118 # ---------------------------------------------------------------------
1119 sub get_circ_policy {
1120 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1123 duration_rule => $duration_rule->name,
1124 recurring_fine_rule => $recurring_fine_rule->name,
1125 max_fine_rule => $max_fine_rule->name,
1126 max_fine => $self->get_max_fine_amount($max_fine_rule),
1127 fine_interval => $recurring_fine_rule->recurrence_interval,
1128 renewal_remaining => $duration_rule->max_renewals,
1129 grace_period => $recurring_fine_rule->grace_period
1132 if($hard_due_date) {
1133 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1134 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1137 $policy->{duration_date_ceiling} = undef;
1138 $policy->{duration_date_ceiling_force} = undef;
1141 $policy->{duration} = $duration_rule->shrt
1142 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1143 $policy->{duration} = $duration_rule->normal
1144 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1145 $policy->{duration} = $duration_rule->extended
1146 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1148 $policy->{recurring_fine} = $recurring_fine_rule->low
1149 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1150 $policy->{recurring_fine} = $recurring_fine_rule->normal
1151 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1152 $policy->{recurring_fine} = $recurring_fine_rule->high
1153 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1158 sub get_max_fine_amount {
1160 my $max_fine_rule = shift;
1161 my $max_amount = $max_fine_rule->amount;
1163 # if is_percent is true then the max->amount is
1164 # use as a percentage of the copy price
1165 if ($U->is_true($max_fine_rule->is_percent)) {
1166 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1167 $max_amount = $price * $max_fine_rule->amount / 100;
1169 $U->ou_ancestor_setting_value(
1171 'circ.max_fine.cap_at_price',
1175 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1176 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1184 sub run_copy_permit_scripts {
1186 my $copy = $self->copy || return;
1190 my $results = $self->run_indb_circ_test;
1191 push @allevents, $self->matrix_test_result_events
1192 unless $self->circ_test_success;
1194 # See if this copy has an alert message
1195 my $ae = $self->check_copy_alert();
1196 push( @allevents, $ae ) if $ae;
1198 # uniquify the events
1199 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1200 @allevents = values %hash;
1202 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1204 $self->push_events(@allevents);
1208 sub check_copy_alert {
1210 return undef if $self->is_renewal;
1211 return OpenILS::Event->new(
1212 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1213 if $self->copy and $self->copy->alert_message;
1219 # --------------------------------------------------------------------------
1220 # If the call is overriding and has permissions to override every collected
1221 # event, the are cleared. Any event that the caller does not have
1222 # permission to override, will be left in the event list and bail_out will
1224 # XXX We need code in here to cancel any holds/transits on copies
1225 # that are being force-checked out
1226 # --------------------------------------------------------------------------
1227 sub override_events {
1229 my @events = @{$self->events};
1230 return unless @events;
1231 my $oargs = $self->override_args;
1233 if(!$self->override) {
1234 return $self->bail_out(1)
1235 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1240 for my $e (@events) {
1241 my $tc = $e->{textcode};
1242 next if $tc eq 'SUCCESS';
1243 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1244 my $ov = "$tc.override";
1245 $logger->info("circulator: attempting to override event: $ov");
1247 return $self->bail_on_events($self->editor->event)
1248 unless( $self->editor->allowed($ov) );
1250 return $self->bail_out(1);
1256 # --------------------------------------------------------------------------
1257 # If there is an open claimsreturn circ on the requested copy, close the
1258 # circ if overriding, otherwise bail out
1259 # --------------------------------------------------------------------------
1260 sub handle_claims_returned {
1262 my $copy = $self->copy;
1264 my $CR = $self->editor->search_action_circulation(
1266 target_copy => $copy->id,
1267 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1268 checkin_time => undef,
1272 return unless ($CR = $CR->[0]);
1276 # - If the caller has set the override flag, we will check the item in
1277 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1279 $CR->checkin_time('now');
1280 $CR->checkin_scan_time('now');
1281 $CR->checkin_lib($self->circ_lib);
1282 $CR->checkin_workstation($self->editor->requestor->wsid);
1283 $CR->checkin_staff($self->editor->requestor->id);
1285 $evt = $self->editor->event
1286 unless $self->editor->update_action_circulation($CR);
1289 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1292 $self->bail_on_events($evt) if $evt;
1297 # --------------------------------------------------------------------------
1298 # This performs the checkout
1299 # --------------------------------------------------------------------------
1303 $self->log_me("do_checkout()");
1305 # make sure perms are good if this isn't a renewal
1306 unless( $self->is_renewal ) {
1307 return $self->bail_on_events($self->editor->event)
1308 unless( $self->editor->allowed('COPY_CHECKOUT') );
1311 # verify the permit key
1312 unless( $self->check_permit_key ) {
1313 if( $self->permit_override ) {
1314 return $self->bail_on_events($self->editor->event)
1315 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1317 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1321 # if this is a non-cataloged circ, build the circ and finish
1322 if( $self->is_noncat ) {
1323 $self->checkout_noncat;
1325 OpenILS::Event->new('SUCCESS',
1326 payload => { noncat_circ => $self->circ }));
1330 if( $self->is_precat ) {
1331 $self->make_precat_copy;
1332 return if $self->bail_out;
1334 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1335 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1338 $self->do_copy_checks;
1339 return if $self->bail_out;
1341 $self->run_checkout_scripts();
1342 return if $self->bail_out;
1344 $self->build_checkout_circ_object();
1345 return if $self->bail_out;
1347 my $modify_to_start = $self->booking_adjusted_due_date();
1348 return if $self->bail_out;
1350 $self->apply_modified_due_date($modify_to_start);
1351 return if $self->bail_out;
1353 return $self->bail_on_events($self->editor->event)
1354 unless $self->editor->create_action_circulation($self->circ);
1356 # refresh the circ to force local time zone for now
1357 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1359 if($self->limit_groups) {
1360 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1363 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1365 return if $self->bail_out;
1367 $self->apply_deposit_fee();
1368 return if $self->bail_out;
1370 $self->handle_checkout_holds();
1371 return if $self->bail_out;
1373 # ------------------------------------------------------------------------------
1374 # Update the patron penalty info in the DB. Run it for permit-overrides
1375 # since the penalties are not updated during the permit phase
1376 # ------------------------------------------------------------------------------
1377 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1379 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1382 if($self->is_renewal) {
1383 # flesh the billing summary for the checked-in circ
1384 $pcirc = $self->editor->retrieve_action_circulation([
1386 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1391 OpenILS::Event->new('SUCCESS',
1393 copy => $U->unflesh_copy($self->copy),
1394 volume => $self->volume,
1395 circ => $self->circ,
1397 holds_fulfilled => $self->fulfilled_holds,
1398 deposit_billing => $self->deposit_billing,
1399 rental_billing => $self->rental_billing,
1400 parent_circ => $pcirc,
1401 patron => ($self->return_patron) ? $self->patron : undef,
1402 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1408 sub apply_deposit_fee {
1410 my $copy = $self->copy;
1412 ($self->is_deposit and not $self->is_deposit_exempt) or
1413 ($self->is_rental and not $self->is_rental_exempt);
1415 return if $self->is_deposit and $self->skip_deposit_fee;
1416 return if $self->is_rental and $self->skip_rental_fee;
1418 my $bill = Fieldmapper::money::billing->new;
1419 my $amount = $copy->deposit_amount;
1423 if($self->is_deposit) {
1424 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1426 $self->deposit_billing($bill);
1428 $billing_type = OILS_BILLING_TYPE_RENTAL;
1430 $self->rental_billing($bill);
1433 $bill->xact($self->circ->id);
1434 $bill->amount($amount);
1435 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1436 $bill->billing_type($billing_type);
1437 $bill->btype($btype);
1438 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1440 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1445 my $copy = $self->copy;
1447 my $stat = $copy->status if ref $copy->status;
1448 my $loc = $copy->location if ref $copy->location;
1449 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1451 $copy->status($stat->id) if $stat;
1452 $copy->location($loc->id) if $loc;
1453 $copy->circ_lib($circ_lib->id) if $circ_lib;
1454 $copy->editor($self->editor->requestor->id);
1455 $copy->edit_date('now');
1456 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1458 return $self->bail_on_events($self->editor->event)
1459 unless $self->editor->update_asset_copy($self->copy);
1461 $copy->status($U->copy_status($copy->status));
1462 $copy->location($loc) if $loc;
1463 $copy->circ_lib($circ_lib) if $circ_lib;
1466 sub update_reservation {
1468 my $reservation = $self->reservation;
1470 my $usr = $reservation->usr;
1471 my $target_rt = $reservation->target_resource_type;
1472 my $target_r = $reservation->target_resource;
1473 my $current_r = $reservation->current_resource;
1475 $reservation->usr($usr->id) if ref $usr;
1476 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1477 $reservation->target_resource($target_r->id) if ref $target_r;
1478 $reservation->current_resource($current_r->id) if ref $current_r;
1480 return $self->bail_on_events($self->editor->event)
1481 unless $self->editor->update_booking_reservation($self->reservation);
1484 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1485 $self->reservation($reservation);
1489 sub bail_on_events {
1490 my( $self, @evts ) = @_;
1491 $self->push_events(@evts);
1495 # ------------------------------------------------------------------------------
1496 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1497 # affects copies that will fulfill holds and CIRC affects all other copies.
1498 # If blocks exists, bail, push Events onto the event pile, and return true.
1499 # ------------------------------------------------------------------------------
1500 sub check_hold_fulfill_blocks {
1503 # With the addition of ignore_proximity in csp, we need to fetch
1504 # the proximity of both the circ_lib and the copy's circ_lib to
1505 # the patron's home_ou.
1506 my ($ou_prox, $copy_prox);
1507 my $home_ou = (ref($self->patron->home_ou)) ? $self->patron->home_ou->id : $self->patron->home_ou;
1508 $ou_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $self->circ_lib);
1509 $ou_prox = -1 unless (defined($ou_prox));
1510 my $copy_ou = (ref($self->copy->circ_lib)) ? $self->copy->circ_lib->id : $self->copy->circ_lib;
1511 if ($copy_ou == $self->circ_lib) {
1512 # Save us the time of an extra query.
1513 $copy_prox = $ou_prox;
1515 $copy_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $copy_ou);
1516 $copy_prox = -1 unless (defined($copy_prox));
1519 # See if the user has any penalties applied that prevent hold fulfillment
1520 my $pens = $self->editor->json_query({
1521 select => {csp => ['name', 'label']},
1522 from => {ausp => {csp => {}}},
1525 usr => $self->patron->id,
1526 org_unit => $U->get_org_full_path($self->circ_lib),
1528 {stop_date => undef},
1529 {stop_date => {'>' => 'now'}}
1533 block_list => {'like' => '%FULFILL%'},
1535 {ignore_proximity => undef},
1536 {ignore_proximity => {'<' => $ou_prox}},
1537 {ignore_proximity => {'<' => $copy_prox}}
1543 return 0 unless @$pens;
1545 for my $pen (@$pens) {
1546 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1547 my $event = OpenILS::Event->new($pen->{name});
1548 $event->{desc} = $pen->{label};
1549 $self->push_events($event);
1552 $self->override_events;
1553 return $self->bail_out;
1557 # ------------------------------------------------------------------------------
1558 # When an item is checked out, see if we can fulfill a hold for this patron
1559 # ------------------------------------------------------------------------------
1560 sub handle_checkout_holds {
1562 my $copy = $self->copy;
1563 my $patron = $self->patron;
1565 my $e = $self->editor;
1566 $self->fulfilled_holds([]);
1568 # non-cats can't fulfill a hold
1569 return if $self->is_noncat;
1571 my $hold = $e->search_action_hold_request({
1572 current_copy => $copy->id ,
1573 cancel_time => undef,
1574 fulfillment_time => undef
1577 if($hold and $hold->usr != $patron->id) {
1578 # reset the hold since the copy is now checked out
1580 $logger->info("circulator: un-targeting hold ".$hold->id.
1581 " because copy ".$copy->id." is getting checked out");
1583 $hold->clear_prev_check_time;
1584 $hold->clear_current_copy;
1585 $hold->clear_capture_time;
1586 $hold->clear_shelf_time;
1587 $hold->clear_shelf_expire_time;
1588 $hold->clear_current_shelf_lib;
1590 return $self->bail_on_event($e->event)
1591 unless $e->update_action_hold_request($hold);
1597 $hold = $self->find_related_user_hold($copy, $patron) or return;
1598 $logger->info("circulator: found related hold to fulfill in checkout");
1601 return if $self->check_hold_fulfill_blocks;
1603 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1605 # if the hold was never officially captured, capture it.
1606 $hold->current_copy($copy->id);
1607 $hold->capture_time('now') unless $hold->capture_time;
1608 $hold->fulfillment_time('now');
1609 $hold->fulfillment_staff($e->requestor->id);
1610 $hold->fulfillment_lib($self->circ_lib);
1612 return $self->bail_on_events($e->event)
1613 unless $e->update_action_hold_request($hold);
1615 return $self->fulfilled_holds([$hold->id]);
1619 # ------------------------------------------------------------------------------
1620 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1621 # the patron directly targets the checked out item, see if there is another hold
1622 # for the patron that could be fulfilled by the checked out item. Fulfill the
1623 # oldest hold and only fulfill 1 of them.
1625 # For "another hold":
1627 # First, check for one that the copy matches via hold_copy_map, ensuring that
1628 # *any* hold type that this copy could fill may end up filled.
1630 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1631 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1632 # that are non-requestable to count as capturing those hold types.
1633 # ------------------------------------------------------------------------------
1634 sub find_related_user_hold {
1635 my($self, $copy, $patron) = @_;
1636 my $e = $self->editor;
1638 # holds on precat copies are always copy-level, so this call will
1639 # always return undef. Exit early.
1640 return undef if $self->is_precat;
1642 return undef unless $U->ou_ancestor_setting_value(
1643 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1645 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1647 select => {ahr => ['id']},
1656 fkey => 'current_copy',
1657 type => 'left' # there may be no current_copy
1664 fulfillment_time => undef,
1665 cancel_time => undef,
1667 {expire_time => undef},
1668 {expire_time => {'>' => 'now'}}
1672 target_copy => $self->copy->id
1676 {id => undef}, # left-join copy may be nonexistent
1677 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1681 order_by => {ahr => {request_time => {direction => 'asc'}}},
1685 my $hold_info = $e->json_query($args)->[0];
1686 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1687 return undef if $U->ou_ancestor_setting_value(
1688 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1690 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1692 select => {ahr => ['id']},
1697 fkey => 'current_copy',
1698 type => 'left' # there may be no current_copy
1705 fulfillment_time => undef,
1706 cancel_time => undef,
1708 {expire_time => undef},
1709 {expire_time => {'>' => 'now'}}
1716 target => $self->volume->id
1722 target => $self->title->id
1728 {id => undef}, # left-join copy may be nonexistent
1729 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1733 order_by => {ahr => {request_time => {direction => 'asc'}}},
1737 $hold_info = $e->json_query($args)->[0];
1738 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1743 sub run_checkout_scripts {
1756 my $hard_due_date_name;
1758 $self->run_indb_circ_test();
1759 $duration = $self->circ_matrix_matchpoint->duration_rule;
1760 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1761 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1762 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1764 $duration_name = $duration->name if $duration;
1765 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1768 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1769 return $self->bail_on_events($evt) if ($evt && !$nobail);
1771 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1772 return $self->bail_on_events($evt) if ($evt && !$nobail);
1774 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1775 return $self->bail_on_events($evt) if ($evt && !$nobail);
1777 if($hard_due_date_name) {
1778 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1779 return $self->bail_on_events($evt) if ($evt && !$nobail);
1785 # The item circulates with an unlimited duration
1789 $hard_due_date = undef;
1792 $self->duration_rule($duration);
1793 $self->recurring_fines_rule($recurring);
1794 $self->max_fine_rule($max_fine);
1795 $self->hard_due_date($hard_due_date);
1799 sub build_checkout_circ_object {
1802 my $circ = Fieldmapper::action::circulation->new;
1803 my $duration = $self->duration_rule;
1804 my $max = $self->max_fine_rule;
1805 my $recurring = $self->recurring_fines_rule;
1806 my $hard_due_date = $self->hard_due_date;
1807 my $copy = $self->copy;
1808 my $patron = $self->patron;
1809 my $duration_date_ceiling;
1810 my $duration_date_ceiling_force;
1814 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1815 $duration_date_ceiling = $policy->{duration_date_ceiling};
1816 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1818 my $dname = $duration->name;
1819 my $mname = $max->name;
1820 my $rname = $recurring->name;
1822 if($hard_due_date) {
1823 $hdname = $hard_due_date->name;
1826 $logger->debug("circulator: building circulation ".
1827 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1829 $circ->duration($policy->{duration});
1830 $circ->recurring_fine($policy->{recurring_fine});
1831 $circ->duration_rule($duration->name);
1832 $circ->recurring_fine_rule($recurring->name);
1833 $circ->max_fine_rule($max->name);
1834 $circ->max_fine($policy->{max_fine});
1835 $circ->fine_interval($recurring->recurrence_interval);
1836 $circ->renewal_remaining($duration->max_renewals);
1837 $circ->grace_period($policy->{grace_period});
1841 $logger->info("circulator: copy found with an unlimited circ duration");
1842 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1843 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1844 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1845 $circ->renewal_remaining(0);
1846 $circ->grace_period(0);
1849 $circ->target_copy( $copy->id );
1850 $circ->usr( $patron->id );
1851 $circ->circ_lib( $self->circ_lib );
1852 $circ->workstation($self->editor->requestor->wsid)
1853 if defined $self->editor->requestor->wsid;
1855 # renewals maintain a link to the parent circulation
1856 $circ->parent_circ($self->parent_circ);
1858 if( $self->is_renewal ) {
1859 $circ->opac_renewal('t') if $self->opac_renewal;
1860 $circ->phone_renewal('t') if $self->phone_renewal;
1861 $circ->desk_renewal('t') if $self->desk_renewal;
1862 $circ->renewal_remaining($self->renewal_remaining);
1863 $circ->circ_staff($self->editor->requestor->id);
1867 # if the user provided an overiding checkout time,
1868 # (e.g. the checkout really happened several hours ago), then
1869 # we apply that here. Does this need a perm??
1870 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1871 if $self->checkout_time;
1873 # if a patron is renewing, 'requestor' will be the patron
1874 $circ->circ_staff($self->editor->requestor->id);
1875 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
1880 sub do_reservation_pickup {
1883 $self->log_me("do_reservation_pickup()");
1885 $self->reservation->pickup_time('now');
1888 $self->reservation->current_resource &&
1889 $U->is_true($self->reservation->target_resource_type->catalog_item)
1891 # We used to try to set $self->copy and $self->patron here,
1892 # but that should already be done.
1894 $self->run_checkout_scripts(1);
1896 my $duration = $self->duration_rule;
1897 my $max = $self->max_fine_rule;
1898 my $recurring = $self->recurring_fines_rule;
1900 if ($duration && $max && $recurring) {
1901 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1903 my $dname = $duration->name;
1904 my $mname = $max->name;
1905 my $rname = $recurring->name;
1907 $logger->debug("circulator: updating reservation ".
1908 "with duration=$dname, maxfine=$mname, recurring=$rname");
1910 $self->reservation->fine_amount($policy->{recurring_fine});
1911 $self->reservation->max_fine($policy->{max_fine});
1912 $self->reservation->fine_interval($recurring->recurrence_interval);
1915 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1916 $self->update_copy();
1919 $self->reservation->fine_amount(
1920 $self->reservation->target_resource_type->fine_amount
1922 $self->reservation->max_fine(
1923 $self->reservation->target_resource_type->max_fine
1925 $self->reservation->fine_interval(
1926 $self->reservation->target_resource_type->fine_interval
1930 $self->update_reservation();
1933 sub do_reservation_return {
1935 my $request = shift;
1937 $self->log_me("do_reservation_return()");
1939 if (not ref $self->reservation) {
1940 my ($reservation, $evt) =
1941 $U->fetch_booking_reservation($self->reservation);
1942 return $self->bail_on_events($evt) if $evt;
1943 $self->reservation($reservation);
1946 $self->handle_fines(1);
1947 $self->reservation->return_time('now');
1948 $self->update_reservation();
1949 $self->reshelve_copy if $self->copy;
1951 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1952 $self->copy( $self->reservation->current_resource->catalog_item );
1956 sub booking_adjusted_due_date {
1958 my $circ = $self->circ;
1959 my $copy = $self->copy;
1961 return undef unless $self->use_booking;
1965 if( $self->due_date ) {
1967 return $self->bail_on_events($self->editor->event)
1968 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1970 $circ->due_date(cleanse_ISO8601($self->due_date));
1974 return unless $copy and $circ->due_date;
1977 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1978 if (@$booking_items) {
1979 my $booking_item = $booking_items->[0];
1980 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1982 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1983 my $shorten_circ_setting = $resource_type->elbow_room ||
1984 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1987 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1988 my $bookings = $booking_ses->request('open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken, {
1989 resource => $booking_item->id
1990 , search_start => 'now'
1991 , search_end => $circ->due_date
1992 , fields => { cancel_time => undef, return_time => undef }
1994 $booking_ses->disconnect;
1996 throw OpenSRF::EX::ERROR ("Improper input arguments") unless defined $bookings;
1997 return $self->bail_on_events($bookings) if ref($bookings) eq 'HASH';
1999 my $dt_parser = DateTime::Format::ISO8601->new;
2000 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2002 for my $bid (@$bookings) {
2004 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2006 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2007 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2009 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2010 if ($booking_start < DateTime->now);
2013 if ($U->is_true($stop_circ_setting)) {
2014 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2016 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2017 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2020 # We set the circ duration here only to affect the logic that will
2021 # later (in a DB trigger) mangle the time part of the due date to
2022 # 11:59pm. Having any circ duration that is not a whole number of
2023 # days is enough to prevent the "correction."
2024 my $new_circ_duration = $due_date->epoch - time;
2025 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2026 $circ->duration("$new_circ_duration seconds");
2028 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2032 return $self->bail_on_events($self->editor->event)
2033 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2039 sub apply_modified_due_date {
2041 my $shift_earlier = shift;
2042 my $circ = $self->circ;
2043 my $copy = $self->copy;
2045 if( $self->due_date ) {
2047 return $self->bail_on_events($self->editor->event)
2048 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2050 $circ->due_date(cleanse_ISO8601($self->due_date));
2054 # if the due_date lands on a day when the location is closed
2055 return unless $copy and $circ->due_date;
2057 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2059 # due-date overlap should be determined by the location the item
2060 # is checked out from, not the owning or circ lib of the item
2061 my $org = $self->circ_lib;
2063 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2064 " with an item due date of ".$circ->due_date );
2066 my $dateinfo = $U->storagereq(
2067 'open-ils.storage.actor.org_unit.closed_date.overlap',
2068 $org, $circ->due_date );
2071 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2072 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2074 # XXX make the behavior more dynamic
2075 # for now, we just push the due date to after the close date
2076 if ($shift_earlier) {
2077 $circ->due_date($dateinfo->{start});
2079 $circ->due_date($dateinfo->{end});
2087 sub create_due_date {
2088 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2090 # if there is a raw time component (e.g. from postgres),
2091 # turn it into an interval that interval_to_seconds can parse
2092 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2094 # for now, use the server timezone. TODO: use workstation org timezone
2095 my $due_date = DateTime->now(time_zone => 'local');
2096 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2098 # add the circ duration
2099 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2102 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2103 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2104 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2109 # return ISO8601 time with timezone
2110 return $due_date->strftime('%FT%T%z');
2115 sub make_precat_copy {
2117 my $copy = $self->copy;
2120 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2122 $copy->editor($self->editor->requestor->id);
2123 $copy->edit_date('now');
2124 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2125 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2126 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2127 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2128 $self->update_copy();
2132 $logger->info("circulator: Creating a new precataloged ".
2133 "copy in checkout with barcode " . $self->copy_barcode);
2135 $copy = Fieldmapper::asset::copy->new;
2136 $copy->circ_lib($self->circ_lib);
2137 $copy->creator($self->editor->requestor->id);
2138 $copy->editor($self->editor->requestor->id);
2139 $copy->barcode($self->copy_barcode);
2140 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2141 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2142 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2144 $copy->dummy_title($self->dummy_title || "");
2145 $copy->dummy_author($self->dummy_author || "");
2146 $copy->dummy_isbn($self->dummy_isbn || "");
2147 $copy->circ_modifier($self->circ_modifier);
2150 # See if we need to override the circ_lib for the copy with a configured circ_lib
2151 # Setting is shortname of the org unit
2152 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2153 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2155 if($precat_circ_lib) {
2156 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2159 $self->bail_on_events($self->editor->event);
2163 $copy->circ_lib($org->id);
2167 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2169 $self->push_events($self->editor->event);
2175 sub checkout_noncat {
2181 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2182 my $count = $self->noncat_count || 1;
2183 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2185 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2189 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2190 $self->editor->requestor->id,
2198 $self->push_events($evt);
2206 # If a copy goes into transit and is then checked in before the transit checkin
2207 # interval has expired, push an event onto the overridable events list.
2208 sub check_transit_checkin_interval {
2211 # only concerned with in-transit items
2212 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2214 # no interval, no problem
2215 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2216 return unless $interval;
2218 # capture the transit so we don't have to fetch it again later during checkin
2220 $self->editor->search_action_transit_copy(
2221 {target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef}
2225 # transit from X to X for whatever reason has no min interval
2226 return if $self->transit->source == $self->transit->dest;
2228 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2229 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2230 my $horizon = $t_start->add(seconds => $seconds);
2232 # See if we are still within the transit checkin forbidden range
2233 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2234 if $horizon > DateTime->now;
2237 # Retarget local holds at checkin
2238 sub checkin_retarget {
2240 return unless $self->retarget_mode and $self->retarget_mode =~ m/retarget/; # Retargeting?
2241 return unless $self->is_checkin; # Renewals need not be checked
2242 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2243 return if $self->is_precat; # No holds for precats
2244 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2245 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2246 my $status = $U->copy_status($self->copy->status);
2247 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2248 # Specifically target items that are likely new (by status ID)
2249 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2250 my $location = $self->copy->location;
2251 if(!ref($location)) {
2252 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2253 $self->copy->location($location);
2255 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2257 # Fetch holds for the bib
2258 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2259 $self->editor->authtoken,
2262 capture_time => undef, # No touching captured holds
2263 frozen => 'f', # Don't bother with frozen holds
2264 pickup_lib => $self->circ_lib # Only holds actually here
2267 # Error? Skip the step.
2268 return if exists $result->{"ilsevent"};
2272 foreach my $holdlist (keys %{$result}) {
2273 push @$holds, @{$result->{$holdlist}};
2276 return if scalar(@$holds) == 0; # No holds, no retargeting
2278 # Check for parts on this copy
2279 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2280 my %parts_hash = ();
2281 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2283 # Loop over holds in request-ish order
2284 # Stage 1: Get them into request-ish order
2285 # Also grab type and target for skipping low hanging ones
2286 $result = $self->editor->json_query({
2287 "select" => { "ahr" => ["id", "hold_type", "target"] },
2288 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2289 "where" => { "id" => $holds },
2291 { "class" => "pgt", "field" => "hold_priority"},
2292 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2293 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2294 { "class" => "ahr", "field" => "request_time"}
2299 if (ref $result eq "ARRAY" and scalar @$result) {
2300 foreach (@{$result}) {
2301 # Copy level, but not this copy?
2302 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2303 and $_->{target} != $self->copy->id);
2304 # Volume level, but not this volume?
2305 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2306 if(@$parts) { # We have parts?
2308 next if ($_->{hold_type} eq 'T');
2309 # Skip part holds for parts not on this copy
2310 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2312 # No parts, no part holds
2313 next if ($_->{hold_type} eq 'P');
2315 # So much for easy stuff, attempt a retarget!
2316 my $tresult = $U->simplereq(
2317 'open-ils.hold-targeter',
2318 'open-ils.hold-targeter.target',
2319 {hold => $_->{id}, find_copy => $self->copy->id}
2321 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2322 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2330 $self->log_me("do_checkin()");
2332 return $self->bail_on_events(
2333 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2336 $self->check_transit_checkin_interval;
2337 $self->checkin_retarget;
2339 # the renew code and mk_env should have already found our circulation object
2340 unless( $self->circ ) {
2342 my $circs = $self->editor->search_action_circulation(
2343 { target_copy => $self->copy->id, checkin_time => undef });
2345 $self->circ($$circs[0]);
2347 # for now, just warn if there are multiple open circs on a copy
2348 $logger->warn("circulator: we have ".scalar(@$circs).
2349 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2352 my $stat = $U->copy_status($self->copy->status)->id;
2354 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2355 # differently if they are already paid for. We need to check for this
2356 # early since overdue generation is potentially affected.
2357 my $dont_change_lost_zero = 0;
2358 if ($stat == OILS_COPY_STATUS_LOST
2359 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2360 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2362 # LOST fine settings are controlled by the copy's circ lib, not the the
2364 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2365 $self->copy->circ_lib->id : $self->copy->circ_lib;
2366 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2367 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2368 $self->editor) || 0;
2370 if ($dont_change_lost_zero) {
2371 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2372 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2375 $self->dont_change_lost_zero($dont_change_lost_zero);
2378 if( $self->checkin_check_holds_shelf() ) {
2379 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2380 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2381 if($self->fake_hold_dest) {
2382 $self->hold->pickup_lib($self->circ_lib);
2384 $self->checkin_flesh_events;
2388 unless( $self->is_renewal ) {
2389 return $self->bail_on_events($self->editor->event)
2390 unless $self->editor->allowed('COPY_CHECKIN');
2393 $self->push_events($self->check_copy_alert());
2394 $self->push_events($self->check_checkin_copy_status());
2396 # if the circ is marked as 'claims returned', add the event to the list
2397 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2398 if ($self->circ and $self->circ->stop_fines
2399 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2401 $self->check_circ_deposit();
2403 # handle the overridable events
2404 $self->override_events unless $self->is_renewal;
2405 return if $self->bail_out;
2407 if( $self->copy and !$self->transit ) {
2409 $self->editor->search_action_transit_copy(
2410 { target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef }
2416 $self->checkin_handle_circ_start;
2417 return if $self->bail_out;
2419 if (!$dont_change_lost_zero) {
2420 # if this circ is LOST and we are configured to generate overdue
2421 # fines for lost items on checkin (to fill the gap between mark
2422 # lost time and when the fines would have naturally stopped), then
2423 # stop_fines is no longer valid and should be cleared.
2425 # stop_fines will be set again during the handle_fines() stage.
2426 # XXX should this setting come from the copy circ lib (like other
2427 # LOST settings), instead of the circulation circ lib?
2428 if ($stat == OILS_COPY_STATUS_LOST) {
2429 $self->circ->clear_stop_fines if
2430 $U->ou_ancestor_setting_value(
2432 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2437 # Set stop_fines when claimed never checked out
2438 $self->circ->stop_fines( OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT ) if( $self->claims_never_checked_out );
2440 # handle fines for this circ, including overdue gen if needed
2441 $self->handle_fines;
2444 $self->checkin_handle_circ_finish;
2445 return if $self->bail_out;
2446 $self->checkin_changed(1);
2448 } elsif( $self->transit ) {
2449 my $hold_transit = $self->process_received_transit;
2450 $self->checkin_changed(1);
2452 if( $self->bail_out ) {
2453 $self->checkin_flesh_events;
2457 if( my $e = $self->check_checkin_copy_status() ) {
2458 # If the original copy status is special, alert the caller
2459 my $ev = $self->events;
2460 $self->events([$e]);
2461 $self->override_events;
2462 return if $self->bail_out;
2466 if( $hold_transit or
2467 $U->copy_status($self->copy->status)->id
2468 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2471 if( $hold_transit ) {
2472 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2474 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2479 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2481 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2482 $self->reshelve_copy(1);
2483 $self->cancelled_hold_transit(1);
2484 $self->notify_hold(0); # don't notify for cancelled holds
2485 $self->fake_hold_dest(0);
2486 return if $self->bail_out;
2488 } elsif ($hold and $hold->hold_type eq 'R') {
2490 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2491 $self->notify_hold(0); # No need to notify
2492 $self->fake_hold_dest(0);
2493 $self->noop(1); # Don't try and capture for other holds/transits now
2494 $self->update_copy();
2495 $hold->fulfillment_time('now');
2496 $self->bail_on_events($self->editor->event)
2497 unless $self->editor->update_action_hold_request($hold);
2501 # hold transited to correct location
2502 if($self->fake_hold_dest) {
2503 $hold->pickup_lib($self->circ_lib);
2505 $self->checkin_flesh_events;
2510 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2512 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2513 " that is in-transit, but there is no transit.. repairing");
2514 $self->reshelve_copy(1);
2515 return if $self->bail_out;
2518 if( $self->is_renewal ) {
2519 $self->finish_fines_and_voiding;
2520 return if $self->bail_out;
2521 $self->push_events(OpenILS::Event->new('SUCCESS'));
2525 # ------------------------------------------------------------------------------
2526 # Circulations and transits are now closed where necessary. Now go on to see if
2527 # this copy can fulfill a hold or needs to be routed to a different location
2528 # ------------------------------------------------------------------------------
2530 my $needed_for_something = 0; # formerly "needed_for_hold"
2532 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2534 if (!$self->remote_hold) {
2535 if ($self->use_booking) {
2536 my $potential_hold = $self->hold_capture_is_possible;
2537 my $potential_reservation = $self->reservation_capture_is_possible;
2539 if ($potential_hold and $potential_reservation) {
2540 $logger->info("circulator: item could fulfill either hold or reservation");
2541 $self->push_events(new OpenILS::Event(
2542 "HOLD_RESERVATION_CONFLICT",
2543 "hold" => $potential_hold,
2544 "reservation" => $potential_reservation
2546 return if $self->bail_out;
2547 } elsif ($potential_hold) {
2548 $needed_for_something =
2549 $self->attempt_checkin_hold_capture;
2550 } elsif ($potential_reservation) {
2551 $needed_for_something =
2552 $self->attempt_checkin_reservation_capture;
2555 $needed_for_something = $self->attempt_checkin_hold_capture;
2558 return if $self->bail_out;
2560 unless($needed_for_something) {
2561 my $circ_lib = (ref $self->copy->circ_lib) ?
2562 $self->copy->circ_lib->id : $self->copy->circ_lib;
2564 if( $self->remote_hold ) {
2565 $circ_lib = $self->remote_hold->pickup_lib;
2566 $logger->warn("circulator: Copy ".$self->copy->barcode.
2567 " is on a remote hold's shelf, sending to $circ_lib");
2570 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2572 my $suppress_transit = 0;
2574 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2575 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2576 if($suppress_transit_source && $suppress_transit_source->{value}) {
2577 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2578 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2579 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2580 $suppress_transit = 1;
2585 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2586 # copy is where it needs to be, either for hold or reshelving
2588 $self->checkin_handle_precat();
2589 return if $self->bail_out;
2592 # copy needs to transit "home", or stick here if it's a floating copy
2594 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2595 my $res = $self->editor->json_query(
2597 'evergreen.can_float',
2598 $self->copy->floating->id,
2599 $self->copy->circ_lib,
2604 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2606 if ($can_float) { # Yep, floating, stick here
2607 $self->checkin_changed(1);
2608 $self->copy->circ_lib( $self->circ_lib );
2611 my $bc = $self->copy->barcode;
2612 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2613 $self->checkin_build_copy_transit($circ_lib);
2614 return if $self->bail_out;
2615 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2619 } else { # no-op checkin
2620 if ($self->copy->floating) { # XXX floating items still stick where they are even with no-op checkin?
2621 my $res = $self->editor->json_query(
2624 'evergreen.can_float',
2625 $self->copy->floating->id,
2626 $self->copy->circ_lib,
2631 if ($res && @$res && $U->is_true($res->[0]->{'evergreen.can_float'})) {
2632 $self->checkin_changed(1);
2633 $self->copy->circ_lib( $self->circ_lib );
2639 if($self->claims_never_checked_out and
2640 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2642 # the item was not supposed to be checked out to the user and should now be marked as missing
2643 $self->copy->status(OILS_COPY_STATUS_MISSING);
2647 $self->reshelve_copy unless $needed_for_something;
2650 return if $self->bail_out;
2652 unless($self->checkin_changed) {
2654 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2655 my $stat = $U->copy_status($self->copy->status)->id;
2657 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2658 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2659 $self->bail_out(1); # no need to commit anything
2663 $self->push_events(OpenILS::Event->new('SUCCESS'))
2664 unless @{$self->events};
2667 $self->finish_fines_and_voiding;
2669 OpenILS::Utils::Penalty->calculate_penalties(
2670 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2672 $self->checkin_flesh_events;
2676 sub finish_fines_and_voiding {
2678 return unless $self->circ;
2680 return unless $self->backdate or $self->void_overdues;
2682 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2683 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2685 my $evt = $CC->void_or_zero_overdues(
2686 $self->editor, $self->circ, {backdate => $self->void_overdues ? undef : $self->backdate, note => $note});
2688 return $self->bail_on_events($evt) if $evt;
2690 # Make sure the circ is open or closed as necessary.
2691 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2692 return $self->bail_on_events($evt) if $evt;
2698 # if a deposit was payed for this item, push the event
2699 sub check_circ_deposit {
2701 return unless $self->circ;
2702 my $deposit = $self->editor->search_money_billing(
2704 xact => $self->circ->id,
2706 }, {idlist => 1})->[0];
2708 $self->push_events(OpenILS::Event->new(
2709 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2714 my $force = $self->force || shift;
2715 my $copy = $self->copy;
2717 my $stat = $U->copy_status($copy->status)->id;
2720 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2721 $stat != OILS_COPY_STATUS_CATALOGING and
2722 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2723 $stat != OILS_COPY_STATUS_RESHELVING )) {
2725 $copy->status( OILS_COPY_STATUS_RESHELVING );
2727 $self->checkin_changed(1);
2732 # Returns true if the item is at the current location
2733 # because it was transited there for a hold and the
2734 # hold has not been fulfilled
2735 sub checkin_check_holds_shelf {
2737 return 0 unless $self->copy;
2740 $U->copy_status($self->copy->status)->id ==
2741 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2743 # Attempt to clear shelf expired holds for this copy
2744 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2745 if($self->clear_expired);
2747 # find the hold that put us on the holds shelf
2748 my $holds = $self->editor->search_action_hold_request(
2750 current_copy => $self->copy->id,
2751 capture_time => { '!=' => undef },
2752 fulfillment_time => undef,
2753 cancel_time => undef,
2758 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2759 $self->reshelve_copy(1);
2763 my $hold = $$holds[0];
2765 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2766 $hold->id. "] for copy ".$self->copy->barcode);
2768 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2769 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2770 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2771 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2772 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2773 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2774 $self->fake_hold_dest(1);
2780 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2781 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2785 $logger->info("circulator: hold is not for here..");
2786 $self->remote_hold($hold);
2791 sub checkin_handle_precat {
2793 my $copy = $self->copy;
2795 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2796 $copy->status(OILS_COPY_STATUS_CATALOGING);
2797 $self->update_copy();
2798 $self->checkin_changed(1);
2799 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2804 sub checkin_build_copy_transit {
2807 my $copy = $self->copy;
2808 my $transit = Fieldmapper::action::transit_copy->new;
2810 # if we are transiting an item to the shelf shelf, it's a hold transit
2811 if (my $hold = $self->remote_hold) {
2812 $transit = Fieldmapper::action::hold_transit_copy->new;
2813 $transit->hold($hold->id);
2815 # the item is going into transit, remove any shelf-iness
2816 if ($hold->current_shelf_lib or $hold->shelf_time) {
2817 $hold->clear_current_shelf_lib;
2818 $hold->clear_shelf_time;
2819 return $self->bail_on_events($self->editor->event)
2820 unless $self->editor->update_action_hold_request($hold);
2824 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2825 $logger->info("circulator: transiting copy to $dest");
2827 $transit->source($self->circ_lib);
2828 $transit->dest($dest);
2829 $transit->target_copy($copy->id);
2830 $transit->source_send_time('now');
2831 $transit->copy_status( $U->copy_status($copy->status)->id );
2833 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2835 if ($self->remote_hold) {
2836 return $self->bail_on_events($self->editor->event)
2837 unless $self->editor->create_action_hold_transit_copy($transit);
2839 return $self->bail_on_events($self->editor->event)
2840 unless $self->editor->create_action_transit_copy($transit);
2843 # ensure the transit is returned to the caller
2844 $self->transit($transit);
2846 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2848 $self->checkin_changed(1);
2852 sub hold_capture_is_possible {
2854 my $copy = $self->copy;
2856 # we've been explicitly told not to capture any holds
2857 return 0 if $self->capture eq 'nocapture';
2859 # See if this copy can fulfill any holds
2860 my $hold = $holdcode->find_nearest_permitted_hold(
2861 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2863 return undef if ref $hold eq "HASH" and
2864 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2868 sub reservation_capture_is_possible {
2870 my $copy = $self->copy;
2872 # we've been explicitly told not to capture any holds
2873 return 0 if $self->capture eq 'nocapture';
2875 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2876 my $resv = $booking_ses->request(
2877 "open-ils.booking.reservations.could_capture",
2878 $self->editor->authtoken, $copy->barcode
2880 $booking_ses->disconnect;
2881 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2882 $self->push_events($resv);
2888 # returns true if the item was used (or may potentially be used
2889 # in subsequent calls) to capture a hold.
2890 sub attempt_checkin_hold_capture {
2892 my $copy = $self->copy;
2894 # we've been explicitly told not to capture any holds
2895 return 0 if $self->capture eq 'nocapture';
2897 # See if this copy can fulfill any holds
2898 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2899 $self->editor, $copy, $self->editor->requestor );
2902 $logger->debug("circulator: no potential permitted".
2903 "holds found for copy ".$copy->barcode);
2907 if($self->capture ne 'capture') {
2908 # see if this item is in a hold-capture-delay location
2909 my $location = $self->copy->location;
2910 if(!ref($location)) {
2911 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2912 $self->copy->location($location);
2914 if($U->is_true($location->hold_verify)) {
2915 $self->bail_on_events(
2916 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2921 $self->retarget($retarget);
2923 my $suppress_transit = 0;
2924 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2925 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2926 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2927 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2928 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2929 $suppress_transit = 1;
2930 $hold->pickup_lib($self->circ_lib);
2935 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2937 $hold->current_copy($copy->id);
2938 $hold->capture_time('now');
2939 $self->put_hold_on_shelf($hold)
2940 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
2942 # prevent DB errors caused by fetching
2943 # holds from storage, and updating through cstore
2944 $hold->clear_fulfillment_time;
2945 $hold->clear_fulfillment_staff;
2946 $hold->clear_fulfillment_lib;
2947 $hold->clear_expire_time;
2948 $hold->clear_cancel_time;
2949 $hold->clear_prev_check_time unless $hold->prev_check_time;
2951 $self->bail_on_events($self->editor->event)
2952 unless $self->editor->update_action_hold_request($hold);
2954 $self->checkin_changed(1);
2956 return 0 if $self->bail_out;
2958 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
2960 if ($hold->hold_type eq 'R') {
2961 $copy->status(OILS_COPY_STATUS_CATALOGING);
2962 $hold->fulfillment_time('now');
2963 $self->noop(1); # Block other transit/hold checks
2964 $self->bail_on_events($self->editor->event)
2965 unless $self->editor->update_action_hold_request($hold);
2967 # This hold was captured in the correct location
2968 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2969 $self->push_events(OpenILS::Event->new('SUCCESS'));
2971 #$self->do_hold_notify($hold->id);
2972 $self->notify_hold($hold->id);
2977 # Hold needs to be picked up elsewhere. Build a hold
2978 # transit and route the item.
2979 $self->checkin_build_hold_transit();
2980 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2981 return 0 if $self->bail_out;
2982 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2985 # make sure we save the copy status
2987 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
2991 sub attempt_checkin_reservation_capture {
2993 my $copy = $self->copy;
2995 # we've been explicitly told not to capture any holds
2996 return 0 if $self->capture eq 'nocapture';
2998 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2999 my $evt = $booking_ses->request(
3000 "open-ils.booking.resources.capture_for_reservation",
3001 $self->editor->authtoken,
3003 1 # don't update copy - we probably have it locked
3005 $booking_ses->disconnect;
3007 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3009 "open-ils.booking.resources.capture_for_reservation " .
3010 "didn't return an event!"
3014 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3015 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3017 # not-transferable is an error event we'll pass on the user
3018 $logger->warn("reservation capture attempted against non-transferable item");
3019 $self->push_events($evt);
3021 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3022 # Re-retrieve copy as reservation capture may have changed
3023 # its status and whatnot.
3025 "circulator: booking capture win on copy " . $self->copy->id
3027 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3029 "circulator: changing copy " . $self->copy->id .
3030 "'s status from " . $self->copy->status . " to " .
3033 $self->copy->status($new_copy_status);
3036 $self->reservation($evt->{"payload"}->{"reservation"});
3038 if (exists $evt->{"payload"}->{"transit"}) {
3042 "org" => $evt->{"payload"}->{"transit"}->dest
3046 $self->checkin_changed(1);
3050 # other results are treated as "nothing to capture"
3054 sub do_hold_notify {
3055 my( $self, $holdid ) = @_;
3057 my $e = new_editor(xact => 1);
3058 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3060 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3061 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3063 $logger->info("circulator: running delayed hold notify process");
3065 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3066 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3068 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3069 hold_id => $holdid, requestor => $self->editor->requestor);
3071 $logger->debug("circulator: built hold notifier");
3073 if(!$notifier->event) {
3075 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3077 my $stat = $notifier->send_email_notify;
3078 if( $stat == '1' ) {
3079 $logger->info("circulator: hold notify succeeded for hold $holdid");
3083 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3086 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3090 sub retarget_holds {
3092 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3093 my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
3094 $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
3095 # no reason to wait for the return value
3099 sub checkin_build_hold_transit {
3102 my $copy = $self->copy;
3103 my $hold = $self->hold;
3104 my $trans = Fieldmapper::action::hold_transit_copy->new;
3106 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3108 $trans->hold($hold->id);
3109 $trans->source($self->circ_lib);
3110 $trans->dest($hold->pickup_lib);
3111 $trans->source_send_time("now");
3112 $trans->target_copy($copy->id);
3114 # when the copy gets to its destination, it will recover
3115 # this status - put it onto the holds shelf
3116 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3118 return $self->bail_on_events($self->editor->event)
3119 unless $self->editor->create_action_hold_transit_copy($trans);
3124 sub process_received_transit {
3126 my $copy = $self->copy;
3127 my $copyid = $self->copy->id;
3129 my $status_name = $U->copy_status($copy->status)->name;
3130 $logger->debug("circulator: attempting transit receive on ".
3131 "copy $copyid. Copy status is $status_name");
3133 my $transit = $self->transit;
3135 # Check if we are in a transit suppress range
3136 my $suppress_transit = 0;
3137 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3138 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3139 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3140 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3141 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3142 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3143 $suppress_transit = 1;
3144 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3148 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3149 # - this item is in-transit to a different location
3150 # - Or we are capturing holds as transits, so why create a new transit?
3152 my $tid = $transit->id;
3153 my $loc = $self->circ_lib;
3154 my $dest = $transit->dest;
3156 $logger->info("circulator: Fowarding transit on copy which is destined ".
3157 "for a different location. transit=$tid, copy=$copyid, current ".
3158 "location=$loc, destination location=$dest");
3160 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3162 # grab the associated hold object if available
3163 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3164 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3166 return $self->bail_on_events($evt);
3169 # The transit is received, set the receive time
3170 $transit->dest_recv_time('now');
3171 $self->bail_on_events($self->editor->event)
3172 unless $self->editor->update_action_transit_copy($transit);
3174 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3176 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3177 $copy->status( $transit->copy_status );
3178 $self->update_copy();
3179 return if $self->bail_out;
3183 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3186 # hold has arrived at destination, set shelf time
3187 $self->put_hold_on_shelf($hold);
3188 $self->bail_on_events($self->editor->event)
3189 unless $self->editor->update_action_hold_request($hold);
3190 return if $self->bail_out;
3192 $self->notify_hold($hold_transit->hold);
3195 $hold_transit = undef;
3196 $self->cancelled_hold_transit(1);
3197 $self->reshelve_copy(1);
3198 $self->fake_hold_dest(0);
3203 OpenILS::Event->new(
3206 payload => { transit => $transit, holdtransit => $hold_transit } ));
3208 return $hold_transit;
3212 # ------------------------------------------------------------------
3213 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3214 # ------------------------------------------------------------------
3215 sub put_hold_on_shelf {
3216 my($self, $hold) = @_;
3217 $hold->shelf_time('now');
3218 $hold->current_shelf_lib($self->circ_lib);
3219 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3225 my $reservation = shift;
3226 my $dt_parser = DateTime::Format::ISO8601->new;
3228 my $obj = $reservation ? $self->reservation : $self->circ;
3230 my $lost_bill_opts = $self->lost_bill_options;
3231 my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3232 # first, restore any voided overdues for lost, if needed
3233 if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3234 my $restore_od = $U->ou_ancestor_setting_value(
3235 $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3236 $self->editor) || 0;
3237 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3241 # next, handle normal overdue generation and apply stop_fines
3242 # XXX reservations don't have stop_fines
3243 # TODO revisit booking_reservation re: stop_fines support
3244 if ($reservation or !$obj->stop_fines) {
3247 # This is a crude check for whether we are in a grace period. The code
3248 # in generate_fines() does a more thorough job, so this exists solely
3249 # as a small optimization, and might be better off removed.
3251 # If we have a grace period
3252 if($obj->can('grace_period')) {
3253 # Parse out the due date
3254 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3255 # Add the grace period to the due date
3256 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3257 # Don't generate fines on circs still in grace period
3258 $skip_for_grace = $due_date > DateTime->now;
3260 $CC->generate_fines({circs => [$obj], editor => $self->editor})
3261 unless $skip_for_grace;
3263 if (!$reservation and !$obj->stop_fines) {
3264 $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3265 $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3266 $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3267 $obj->stop_fines_time('now');
3268 $obj->stop_fines_time($self->backdate) if $self->backdate;
3269 $self->editor->update_action_circulation($obj);
3273 # finally, handle voiding of lost item and processing fees
3274 if ($self->needs_lost_bill_handling) {
3275 my $void_cost = $U->ou_ancestor_setting_value(
3276 $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3277 $self->editor) || 0;
3278 my $void_proc_fee = $U->ou_ancestor_setting_value(
3279 $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3280 $self->editor) || 0;
3281 $self->checkin_handle_lost_or_lo_now_found(
3282 $lost_bill_opts->{void_cost_btype},
3283 $lost_bill_opts->{is_longoverdue}) if $void_cost;
3284 $self->checkin_handle_lost_or_lo_now_found(
3285 $lost_bill_opts->{void_fee_btype},
3286 $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3292 sub checkin_handle_circ_start {
3294 my $circ = $self->circ;
3295 my $copy = $self->copy;
3299 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3301 # backdate the circ if necessary
3302 if($self->backdate) {
3303 my $evt = $self->checkin_handle_backdate;
3304 return $self->bail_on_events($evt) if $evt;
3307 # Set the checkin vars since we have the item
3308 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3310 # capture the true scan time for back-dated checkins
3311 $circ->checkin_scan_time('now');
3313 $circ->checkin_staff($self->editor->requestor->id);
3314 $circ->checkin_lib($self->circ_lib);
3315 $circ->checkin_workstation($self->editor->requestor->wsid);
3317 my $circ_lib = (ref $self->copy->circ_lib) ?
3318 $self->copy->circ_lib->id : $self->copy->circ_lib;
3319 my $stat = $U->copy_status($self->copy->status)->id;
3321 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3322 # we will now handle lost fines, but the copy will retain its 'lost'
3323 # status if it needs to transit home unless lost_immediately_available
3326 # if we decide to also delay fine handling until the item arrives home,
3327 # we will need to call lost fine handling code both when checking items
3328 # in and also when receiving transits
3329 $self->checkin_handle_lost($circ_lib);
3330 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3331 # same process as above.
3332 $self->checkin_handle_long_overdue($circ_lib);
3333 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3334 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3336 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3343 sub checkin_handle_circ_finish {
3345 my $e = $self->editor;
3346 my $circ = $self->circ;
3348 # Do one last check before the final circulation update to see
3349 # if the xact_finish value should be set or not.
3351 # The underlying money.billable_xact may have been updated to
3352 # reflect a change in xact_finish during checkin bills handling,
3353 # however we can't simply refresh the circulation from the DB,
3354 # because other changes may be pending. Instead, reproduce the
3355 # xact_finish check here. It won't hurt to do it again.
3357 my $sum = $e->retrieve_money_billable_transaction_summary($circ->id);
3358 if ($sum) { # is this test still needed?
3360 my $balance = $sum->balance_owed;
3362 if ($balance == 0) {
3363 $circ->xact_finish('now');
3365 $circ->clear_xact_finish;
3368 $logger->info("circulator: $balance is owed on this circulation");
3371 return $self->bail_on_events($e->event)
3372 unless $e->update_action_circulation($circ);
3377 # ------------------------------------------------------------------
3378 # See if we need to void billings, etc. for lost checkin
3379 # ------------------------------------------------------------------
3380 sub checkin_handle_lost {
3382 my $circ_lib = shift;
3384 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3385 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3387 $self->lost_bill_options({
3388 circ_lib => $circ_lib,
3389 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3390 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3391 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3392 void_cost_btype => 3,
3396 return $self->checkin_handle_lost_or_longoverdue(
3397 circ_lib => $circ_lib,
3398 max_return => $max_return,
3399 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3400 ous_use_last_activity => undef # not supported for LOST checkin
3404 # ------------------------------------------------------------------
3405 # See if we need to void billings, etc. for long-overdue checkin
3406 # note: not using constants below since they serve little purpose
3407 # for single-use strings that are descriptive in their own right
3408 # and mostly just complicate debugging.
3409 # ------------------------------------------------------------------
3410 sub checkin_handle_long_overdue {
3412 my $circ_lib = shift;
3414 $logger->info("circulator: processing long-overdue checkin...");
3416 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3417 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3419 $self->lost_bill_options({
3420 circ_lib => $circ_lib,
3421 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3422 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3423 is_longoverdue => 1,
3424 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3425 void_cost_btype => 10,
3426 void_fee_btype => 11
3429 return $self->checkin_handle_lost_or_longoverdue(
3430 circ_lib => $circ_lib,
3431 max_return => $max_return,
3432 ous_immediately_available => 'circ.longoverdue_immediately_available',
3433 ous_use_last_activity =>
3434 'circ.longoverdue.use_last_activity_date_on_return'
3438 # last billing activity is last payment time, last billing time, or the
3439 # circ due date. If the relevant "use last activity" org unit setting is
3440 # false/unset, then last billing activity is always the due date.
3441 sub get_circ_last_billing_activity {
3443 my $circ_lib = shift;
3444 my $setting = shift;
3445 my $date = $self->circ->due_date;
3447 return $date unless $setting and
3448 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3450 my $xact = $self->editor->retrieve_money_billable_transaction([
3452 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3455 if ($xact->summary) {
3456 $date = $xact->summary->last_payment_ts ||
3457 $xact->summary->last_billing_ts ||
3458 $self->circ->due_date;
3465 sub checkin_handle_lost_or_longoverdue {
3466 my ($self, %args) = @_;
3468 my $circ = $self->circ;
3469 my $max_return = $args{max_return};
3470 my $circ_lib = $args{circ_lib};
3475 $self->get_circ_last_billing_activity(
3476 $circ_lib, $args{ous_use_last_activity});
3479 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3480 $tm[5] -= 1 if $tm[5] > 0;
3481 my $due = timelocal(int($tm[1]), int($tm[2]),
3482 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3485 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3487 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3488 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3489 "DUE: $due LAST: $last_chance");
3491 $max_return = 0 if $today < $last_chance;
3497 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3498 "return interval. skipping fine/fee voiding, etc.");
3500 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3502 $logger->info("circulator: check-in of lost/lo item having a balance ".
3503 "of zero, skipping fine/fee voiding and reinstatement.");
3505 } else { # within max-return interval or no interval defined
3507 $logger->info("circulator: check-in of lost/lo item is within the ".
3508 "max return interval (or no interval is defined). Proceeding ".
3509 "with fine/fee voiding, etc.");
3511 $self->needs_lost_bill_handling(1);
3514 if ($circ_lib != $self->circ_lib) {
3515 # if the item is not home, check to see if we want to retain the
3516 # lost/longoverdue status at this point in the process
3518 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3519 $args{ous_immediately_available}, $self->editor) || 0;
3521 if ($immediately_available) {
3522 # item status does not need to be retained, so give it a
3523 # reshelving status as if it were a normal checkin
3524 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3527 $logger->info("circulator: leaving lost/longoverdue copy".
3528 " status in place on checkin");
3531 # lost/longoverdue item is home and processed, treat like a normal
3532 # checkin from this point on
3533 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3539 sub checkin_handle_backdate {
3542 # ------------------------------------------------------------------
3543 # clean up the backdate for date comparison
3544 # XXX We are currently taking the due-time from the original due-date,
3545 # not the input. Do we need to do this? This certainly interferes with
3546 # backdating of hourly checkouts, but that is likely a very rare case.
3547 # ------------------------------------------------------------------
3548 my $bd = cleanse_ISO8601($self->backdate);
3549 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3550 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3551 $new_date->set_hour($original_date->hour());
3552 $new_date->set_minute($original_date->minute());
3553 if ($new_date >= DateTime->now) {
3554 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3555 # $self->backdate() autoload handler ignores undef values.
3556 # Clear the backdate manually.
3557 $logger->info("circulator: ignoring future backdate: $new_date");
3558 delete $self->{backdate};
3560 $self->backdate(cleanse_ISO8601($new_date->datetime()));
3567 sub check_checkin_copy_status {
3569 my $copy = $self->copy;
3571 my $status = $U->copy_status($copy->status)->id;
3574 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3575 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3576 $status == OILS_COPY_STATUS_IN_PROCESS ||
3577 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3578 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3579 $status == OILS_COPY_STATUS_CATALOGING ||
3580 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3581 $status == OILS_COPY_STATUS_CANCELED_TRANSIT ||
3582 $status == OILS_COPY_STATUS_RESHELVING );
3584 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3585 if( $status == OILS_COPY_STATUS_LOST );
3587 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3588 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3590 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3591 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3593 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3594 if( $status == OILS_COPY_STATUS_MISSING );
3596 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3601 # --------------------------------------------------------------------------
3602 # On checkin, we need to return as many relevant objects as we can
3603 # --------------------------------------------------------------------------
3604 sub checkin_flesh_events {
3607 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3608 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3609 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3612 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3615 if($self->hold and !$self->hold->cancel_time) {
3616 $hold = $self->hold;
3617 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3621 # update our copy of the circ object and
3622 # flesh the billing summary data
3624 $self->editor->retrieve_action_circulation([
3628 circ => ['billable_transaction'],
3637 # flesh some patron fields before returning
3639 $self->editor->retrieve_actor_user([
3644 au => ['card', 'billing_address', 'mailing_address']
3651 for my $evt (@{$self->events}) {
3654 $payload->{copy} = $U->unflesh_copy($self->copy);
3655 $payload->{volume} = $self->volume;
3656 $payload->{record} = $record,
3657 $payload->{circ} = $self->circ;
3658 $payload->{transit} = $self->transit;
3659 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3660 $payload->{hold} = $hold;
3661 $payload->{patron} = $self->patron;
3662 $payload->{reservation} = $self->reservation
3663 unless (not $self->reservation or $self->reservation->cancel_time);
3665 $evt->{payload} = $payload;
3670 my( $self, $msg ) = @_;
3671 my $bc = ($self->copy) ? $self->copy->barcode :
3674 my $usr = ($self->patron) ? $self->patron->id : "";
3675 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3676 ", recipient=$usr, copy=$bc");
3682 $self->log_me("do_renew()");
3684 # Make sure there is an open circ to renew
3685 my $usrid = $self->patron->id if $self->patron;
3686 my $circ = $self->editor->search_action_circulation({
3687 target_copy => $self->copy->id,
3688 xact_finish => undef,
3689 checkin_time => undef,
3690 ($usrid ? (usr => $usrid) : ())
3693 return $self->bail_on_events($self->editor->event) unless $circ;
3695 # A user is not allowed to renew another user's items without permission
3696 unless( $circ->usr eq $self->editor->requestor->id ) {
3697 return $self->bail_on_events($self->editor->events)
3698 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3701 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3702 if $circ->renewal_remaining < 1;
3704 # -----------------------------------------------------------------
3706 $self->parent_circ($circ->id);
3707 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3710 # Opac renewal - re-use circ library from original circ (unless told not to)
3711 if($self->opac_renewal) {
3712 unless(defined($opac_renewal_use_circ_lib)) {
3713 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3714 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3715 $opac_renewal_use_circ_lib = 1;
3718 $opac_renewal_use_circ_lib = 0;
3721 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3724 # Desk renewal - re-use circ library from original circ (unless told not to)
3725 if($self->desk_renewal) {
3726 unless(defined($desk_renewal_use_circ_lib)) {
3727 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
3728 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3729 $desk_renewal_use_circ_lib = 1;
3732 $desk_renewal_use_circ_lib = 0;
3735 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
3738 # Run the fine generator against the old circ
3739 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
3740 # a few lines down. Commenting out, for now.
3741 #$self->handle_fines;
3743 $self->run_renew_permit;
3746 $self->do_checkin();
3747 return if $self->bail_out;
3749 unless( $self->permit_override ) {
3751 return if $self->bail_out;
3752 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3753 $self->remove_event('ITEM_NOT_CATALOGED');
3756 $self->override_events;
3757 return if $self->bail_out;
3760 $self->do_checkout();
3765 my( $self, $evt ) = @_;
3766 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3767 $logger->debug("circulator: removing event from list: $evt");
3768 my @events = @{$self->events};
3769 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3774 my( $self, $evt ) = @_;
3775 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3776 return grep { $_->{textcode} eq $evt } @{$self->events};
3780 sub run_renew_permit {
3783 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3784 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3785 $self->editor, $self->copy, $self->editor->requestor, 1
3787 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3790 my $results = $self->run_indb_circ_test;
3791 $self->push_events($self->matrix_test_result_events)
3792 unless $self->circ_test_success;
3796 # XXX: The primary mechanism for storing circ history is now handled
3797 # by tracking real circulation objects instead of bibs in a bucket.
3798 # However, this code is disabled by default and could be useful
3799 # some day, so may as well leave it for now.
3800 sub append_reading_list {
3804 $self->is_checkout and
3810 # verify history is globally enabled and uses the bucket mechanism
3811 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3812 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3814 return undef unless $htype and $htype eq 'bucket';
3816 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3818 # verify the patron wants to retain the hisory
3819 my $setting = $e->search_actor_user_setting(
3820 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3822 unless($setting and $setting->value) {
3827 my $bkt = $e->search_container_copy_bucket(
3828 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3833 # find the next item position
3834 my $last_item = $e->search_container_copy_bucket_item(
3835 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3836 $pos = $last_item->pos + 1 if $last_item;
3839 # create the history bucket if necessary
3840 $bkt = Fieldmapper::container::copy_bucket->new;
3841 $bkt->owner($self->patron->id);
3843 $bkt->btype('circ_history');
3845 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3848 my $item = Fieldmapper::container::copy_bucket_item->new;
3850 $item->bucket($bkt->id);
3851 $item->target_copy($self->copy->id);
3854 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3861 sub make_trigger_events {
3863 return unless $self->circ;
3864 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3865 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3866 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3871 sub checkin_handle_lost_or_lo_now_found {
3872 my ($self, $bill_type, $is_longoverdue) = @_;
3874 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3876 $logger->debug("voiding $tag item billings");
3877 my $result = $CC->void_or_zero_bills_of_type($self->editor, $self->circ, $self->copy, $bill_type, "$tag ITEM RETURNED");
3878 $self->bail_on_events($self->editor->event) if ($result);
3881 sub checkin_handle_lost_or_lo_now_found_restore_od {
3883 my $circ_lib = shift;
3884 my $is_longoverdue = shift;
3885 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
3887 # ------------------------------------------------------------------
3888 # restore those overdue charges voided when item was set to lost
3889 # ------------------------------------------------------------------
3891 my $ods = $self->editor->search_money_billing([
3893 xact => $self->circ->id,
3897 order_by => {mb => 'billing_ts desc'}
3901 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
3902 # Because actual users get up to all kinds of unexpectedness, we
3903 # only recreate up to $circ->max_fine in bills. I know you think
3904 # it wouldn't happen that bills could get created, voided, and
3905 # recreated more than once, but I guaran-damn-tee you that it will
3907 if ($ods && @$ods) {
3908 my $void_amount = 0;
3909 my $void_max = $self->circ->max_fine();
3910 # search for overdues voided the new way (aka "adjusted")
3911 my @billings = map {$_->id()} @$ods;
3912 my $voids = $self->editor->search_money_account_adjustment(
3914 billing => \@billings
3918 map {$void_amount += $_->amount()} @$voids;
3920 # if no adjustments found, assume they were voided the old way (aka "voided")
3921 for my $bill (@$ods) {
3922 if( $U->is_true($bill->voided) ) {
3923 $void_amount += $bill->amount();
3929 ($void_amount < $void_max ? $void_amount : $void_max),
3931 $ods->[0]->billing_type(),
3933 "System: $tag RETURNED - OVERDUES REINSTATED",
3934 $ods->[0]->billing_ts() # date this restoration the same as the last overdue (for possible subsequent fine generation)