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']}
43 # table of cases where suppressing a system-generated copy alerts
44 # should generate an override of an old-style event
45 my %COPY_ALERT_OVERRIDES = (
46 "CLAIMSRETURNED\tCHECKOUT" => ['CIRC_CLAIMS_RETURNED'],
47 "CLAIMSRETURNED\tCHECKIN" => ['CIRC_CLAIMS_RETURNED'],
48 "LOST\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'],
49 "LONGOVERDUE\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'],
50 "MISSING\tCHECKOUT" => ['COPY_NOT_AVAILABLE'],
51 "DAMAGED\tCHECKOUT" => ['COPY_NOT_AVAILABLE'],
52 "LOST_AND_PAID\tCHECKOUT" => ['COPY_NOT_AVAILABLE', 'OPEN_CIRCULATION_EXISTS']
57 __PACKAGE__->register_method(
58 method => "run_method",
59 api_name => "open-ils.circ.checkout.permit",
61 Determines if the given checkout can occur
62 @param authtoken The login session key
63 @param params A trailing hash of named params including
64 barcode : The copy barcode,
65 patron : The patron the checkout is occurring for,
66 renew : true or false - whether or not this is a renewal
67 @return The event that occurred during the permit check.
71 __PACKAGE__->register_method (
72 method => 'run_method',
73 api_name => 'open-ils.circ.checkout.permit.override',
74 signature => q/@see open-ils.circ.checkout.permit/,
78 __PACKAGE__->register_method(
79 method => "run_method",
80 api_name => "open-ils.circ.checkout",
83 @param authtoken The login session key
84 @param params A named hash of params including:
86 barcode If no copy is provided, the copy is retrieved via barcode
87 copyid If no copy or barcode is provide, the copy id will be use
88 patron The patron's id
89 noncat True if this is a circulation for a non-cataloted item
90 noncat_type The non-cataloged type id
91 noncat_circ_lib The location for the noncat circ.
92 precat The item has yet to be cataloged
93 dummy_title The temporary title of the pre-cataloded item
94 dummy_author The temporary authr of the pre-cataloded item
95 Default is the home org of the staff member
96 @return The SUCCESS event on success, any other event depending on the error
99 __PACKAGE__->register_method(
100 method => "run_method",
101 api_name => "open-ils.circ.checkin",
104 Generic super-method for handling all copies
105 @param authtoken The login session key
106 @param params Hash of named parameters including:
107 barcode - The copy barcode
108 force - If true, copies in bad statuses will be checked in and give good statuses
109 noop - don't capture holds or put items into transit
110 void_overdues - void all overdues for the circulation (aka amnesty)
115 __PACKAGE__->register_method(
116 method => "run_method",
117 api_name => "open-ils.circ.checkin.override",
118 signature => q/@see open-ils.circ.checkin/
121 __PACKAGE__->register_method(
122 method => "run_method",
123 api_name => "open-ils.circ.renew.override",
124 signature => q/@see open-ils.circ.renew/,
127 __PACKAGE__->register_method(
128 method => "run_method",
129 api_name => "open-ils.circ.renew.auto",
130 signature => q/@see open-ils.circ.renew/,
133 __PACKAGE__->register_method(
134 method => "run_method",
135 api_name => "open-ils.circ.renew",
136 notes => <<" NOTES");
137 PARAMS( authtoken, circ => circ_id );
138 open-ils.circ.renew(login_session, circ_object);
139 Renews the provided circulation. login_session is the requestor of the
140 renewal and if the logged in user is not the same as circ->usr, then
141 the logged in user must have RENEW_CIRC permissions.
144 __PACKAGE__->register_method(
145 method => "run_method",
146 api_name => "open-ils.circ.checkout.full"
148 __PACKAGE__->register_method(
149 method => "run_method",
150 api_name => "open-ils.circ.checkout.full.override"
152 __PACKAGE__->register_method(
153 method => "run_method",
154 api_name => "open-ils.circ.reservation.pickup"
156 __PACKAGE__->register_method(
157 method => "run_method",
158 api_name => "open-ils.circ.reservation.return"
160 __PACKAGE__->register_method(
161 method => "run_method",
162 api_name => "open-ils.circ.reservation.return.override"
164 __PACKAGE__->register_method(
165 method => "run_method",
166 api_name => "open-ils.circ.checkout.inspect",
167 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
172 my( $self, $conn, $auth, $args ) = @_;
173 translate_legacy_args($args);
174 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
175 $args->{new_copy_alerts} ||= $self->api_level > 1 ? 1 : 0;
176 my $api = $self->api_name;
179 OpenILS::Application::Circ::Circulator->new($auth, %$args);
181 return circ_events($circulator) if $circulator->bail_out;
183 $circulator->use_booking(determine_booking_status());
185 # --------------------------------------------------------------------------
186 # First, check for a booking transit, as the barcode may not be a copy
187 # barcode, but a resource barcode, and nothing else in here will work
188 # --------------------------------------------------------------------------
190 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
191 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
192 if (@$resources) { # yes!
194 my $res_id_list = [ map { $_->id } @$resources ];
195 my $transit = $circulator->editor->search_action_reservation_transit_copy(
197 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef, cancel_time => undef },
198 { order_by => { artc => 'source_send_time' }, limit => 1 }
200 )->[0]; # Any transit for this barcode?
202 if ($transit) { # yes! unwrap it.
204 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
205 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
207 my $success_event = new OpenILS::Event(
208 "SUCCESS", "payload" => {"reservation" => $reservation}
210 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
211 if (my $copy = $circulator->editor->search_asset_copy([
212 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
213 ])->[0]) { # got a copy
214 $copy->status( $transit->copy_status );
215 $copy->editor($circulator->editor->requestor->id);
216 $copy->edit_date('now');
217 $circulator->editor->update_asset_copy($copy);
218 $success_event->{"payload"}->{"record"} =
219 $U->record_to_mvr($copy->call_number->record);
220 $success_event->{"payload"}->{"volume"} = $copy->call_number;
221 $copy->call_number($copy->call_number->id);
222 $success_event->{"payload"}->{"copy"} = $copy;
226 $transit->dest_recv_time('now');
227 $circulator->editor->update_action_reservation_transit_copy( $transit );
229 $circulator->editor->commit;
230 # Formerly this branch just stopped here. Argh!
231 $conn->respond_complete($success_event);
237 if ($circulator->use_booking) {
238 $circulator->is_res_checkin($circulator->is_checkin(1))
239 if $api =~ /reservation.return/ or (
240 $api =~ /checkin/ and $circulator->seems_like_reservation()
243 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
246 $circulator->is_renewal(1) if $api =~ /renew/;
247 $circulator->is_autorenewal(1) if $api =~ /renew.auto/;
248 $circulator->is_checkin(1) if $api =~ /checkin/;
249 $circulator->is_checkout(1) if $api =~ /checkout/;
250 $circulator->override(1) if $api =~ /override/o;
252 $circulator->mk_env();
253 $circulator->noop(1) if $circulator->claims_never_checked_out;
255 return circ_events($circulator) if $circulator->bail_out;
257 if( $api =~ /checkout\.permit/ ) {
258 $circulator->do_permit();
260 } elsif( $api =~ /checkout.full/ ) {
262 # requesting a precat checkout implies that any required
263 # overrides have been performed. Go ahead and re-override.
264 $circulator->skip_permit_key(1);
265 $circulator->override(1) if ( $circulator->request_precat && $circulator->editor->allowed('CREATE_PRECAT') );
266 $circulator->do_permit();
267 $circulator->is_checkout(1);
268 unless( $circulator->bail_out ) {
269 $circulator->events([]);
270 $circulator->do_checkout();
273 } elsif( $circulator->is_res_checkout ) {
274 $circulator->do_reservation_pickup();
276 } elsif( $api =~ /inspect/ ) {
277 my $data = $circulator->do_inspect();
278 $circulator->editor->rollback;
281 } elsif( $api =~ /checkout/ ) {
282 $circulator->do_checkout();
284 } elsif( $circulator->is_res_checkin ) {
285 $circulator->do_reservation_return();
286 $circulator->do_checkin() if ($circulator->copy());
287 } elsif( $api =~ /checkin/ ) {
288 $circulator->do_checkin();
290 } elsif( $api =~ /renew/ ) {
291 $circulator->do_renew($api);
294 if( $circulator->bail_out ) {
297 # make sure no success event accidentally slip in
299 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
302 my @e = @{$circulator->events};
303 push( @ee, $_->{textcode} ) for @e;
304 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
306 $circulator->editor->rollback;
310 # checkin and reservation return can result in modifications to
311 # actor.usr.claims_never_checked_out_count without also modifying
312 # actor.last_xact_id. Perform a no-op update on the patron to
313 # force an update to last_xact_id.
314 if ($circulator->claims_never_checked_out && $circulator->patron) {
315 $circulator->editor->update_actor_user(
316 $circulator->editor->retrieve_actor_user($circulator->patron->id))
317 or return $circulator->editor->die_event;
320 $circulator->editor->commit;
323 $conn->respond_complete(circ_events($circulator));
325 return undef if $circulator->bail_out;
327 $circulator->do_hold_notify($circulator->notify_hold)
328 if $circulator->notify_hold;
329 $circulator->retarget_holds if $circulator->retarget;
330 $circulator->append_reading_list;
331 $circulator->make_trigger_events;
338 my @e = @{$circ->events};
339 # if we have multiple events, SUCCESS should not be one of them;
340 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
341 return (@e == 1) ? $e[0] : \@e;
345 sub translate_legacy_args {
348 if( $$args{barcode} ) {
349 $$args{copy_barcode} = $$args{barcode};
350 delete $$args{barcode};
353 if( $$args{copyid} ) {
354 $$args{copy_id} = $$args{copyid};
355 delete $$args{copyid};
358 if( $$args{patronid} ) {
359 $$args{patron_id} = $$args{patronid};
360 delete $$args{patronid};
363 if( $$args{patron} and !ref($$args{patron}) ) {
364 $$args{patron_id} = $$args{patron};
365 delete $$args{patron};
369 if( $$args{noncat} ) {
370 $$args{is_noncat} = $$args{noncat};
371 delete $$args{noncat};
374 if( $$args{precat} ) {
375 $$args{is_precat} = $$args{request_precat} = $$args{precat};
376 delete $$args{precat};
382 # --------------------------------------------------------------------------
383 # This package actually manages all of the circulation logic
384 # --------------------------------------------------------------------------
385 package OpenILS::Application::Circ::Circulator;
386 use strict; use warnings;
387 use vars q/$AUTOLOAD/;
389 use OpenILS::Utils::Fieldmapper;
390 use OpenSRF::Utils::Cache;
391 use Digest::MD5 qw(md5_hex);
392 use DateTime::Format::ISO8601;
393 use OpenILS::Utils::PermitHold;
394 use OpenILS::Utils::DateTime qw/:datetime/;
395 use OpenSRF::Utils::SettingsClient;
396 use OpenILS::Application::Circ::Holds;
397 use OpenILS::Application::Circ::Transit;
398 use OpenSRF::Utils::Logger qw(:logger);
399 use OpenILS::Utils::CStoreEditor qw/:funcs/;
400 use OpenILS::Const qw/:const/;
401 use OpenILS::Utils::Penalty;
402 use OpenILS::Application::Circ::CircCommon;
405 my $CC = "OpenILS::Application::Circ::CircCommon";
406 my $holdcode = "OpenILS::Application::Circ::Holds";
407 my $transcode = "OpenILS::Application::Circ::Transit";
413 # --------------------------------------------------------------------------
414 # Add a pile of automagic getter/setter methods
415 # --------------------------------------------------------------------------
416 my @AUTOLOAD_FIELDS = qw/
429 overrides_per_copy_alerts
471 recurring_fines_level
476 auto_renewal_remaining
485 cancelled_hold_transit
492 circ_matrix_matchpoint
503 claims_never_checked_out
516 dont_change_lost_zero
518 needs_lost_bill_handling
524 my $type = ref($self) or die "$self is not an object";
526 my $name = $AUTOLOAD;
529 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
530 $logger->error("circulator: $type: invalid autoload field: $name");
531 die "$type: invalid autoload field: $name\n"
536 *{"${type}::${name}"} = sub {
539 $s->{$name} = $v if defined $v;
543 return $self->$name($data);
548 my( $class, $auth, %args ) = @_;
549 $class = ref($class) || $class;
550 my $self = bless( {}, $class );
553 $self->editor(new_editor(xact => 1, authtoken => $auth));
555 unless( $self->editor->checkauth ) {
556 $self->bail_on_events($self->editor->event);
560 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
562 $self->$_($args{$_}) for keys %args;
565 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
567 # if this is a renewal, default to desk_renewal
568 $self->desk_renewal(1) unless
569 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
571 $self->capture('') unless $self->capture;
573 unless(%user_groups) {
574 my $gps = $self->editor->retrieve_all_permission_grp_tree;
575 %user_groups = map { $_->id => $_ } @$gps;
582 # --------------------------------------------------------------------------
583 # True if we should discontinue processing
584 # --------------------------------------------------------------------------
586 my( $self, $bool ) = @_;
587 if( defined $bool ) {
588 $logger->info("circulator: BAILING OUT") if $bool;
589 $self->{bail_out} = $bool;
591 return $self->{bail_out};
596 my( $self, @evts ) = @_;
599 $e->{payload} = $self->copy if
600 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
602 $logger->info("circulator: pushing event ".$e->{textcode});
603 push( @{$self->events}, $e ) unless
604 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
610 return '' if $self->skip_permit_key;
611 my $key = md5_hex( time() . rand() . "$$" );
612 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
613 return $self->permit_key($key);
616 sub check_permit_key {
618 return 1 if $self->skip_permit_key;
619 my $key = $self->permit_key;
620 return 0 unless $key;
621 my $k = "oils_permit_key_$key";
622 my $one = $self->cache_handle->get_cache($k);
623 $self->cache_handle->delete_cache($k);
624 return ($one) ? 1 : 0;
627 sub seems_like_reservation {
630 # Some words about the following method:
631 # 1) It requires the VIEW_USER permission, but that's not an
632 # issue, right, since all staff should have that?
633 # 2) It returns only one reservation at a time, even if an item can be
634 # and is currently overbooked. Hmmm....
635 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
636 my $result = $booking_ses->request(
637 "open-ils.booking.reservations.by_returnable_resource_barcode",
638 $self->editor->authtoken,
641 $booking_ses->disconnect;
643 return $self->bail_on_events($result) if defined $U->event_code($result);
646 $self->reservation(shift @$result);
654 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
655 sub save_trimmed_copy {
656 my ($self, $copy) = @_;
659 $self->volume($copy->call_number);
660 $self->title($self->volume->record);
661 $self->copy->call_number($self->volume->id);
662 $self->volume->record($self->title->id);
663 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
664 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
665 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
666 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
670 sub collect_user_copy_alerts {
672 my $e = $self->editor;
675 my $alerts = $e->search_asset_copy_alert([
676 {copy => $self->copy->id, ack_time => undef},
677 {flesh => 1, flesh_fields => { aca => [ qw/ alert_type / ] }}
679 if (ref $alerts eq "ARRAY") {
680 $logger->info("circulator: found " . scalar(@$alerts) . " alerts for copy " .
682 $self->user_copy_alerts($alerts);
687 sub filter_user_copy_alerts {
690 my $e = $self->editor;
692 if(my $alerts = $self->user_copy_alerts) {
694 my $suppress_orgs = $U->get_org_full_path($self->circ_lib);
695 my $suppressions = $e->search_actor_copy_alert_suppress(
696 {org => $suppress_orgs}
700 foreach my $a (@$alerts) {
701 # filter on event type
702 if (defined $a->alert_type) {
703 next if ($a->alert_type->event eq 'CHECKIN' && !$self->is_checkin && !$self->is_renewal);
704 next if ($a->alert_type->event eq 'CHECKOUT' && !$self->is_checkout && !$self->is_renewal);
705 next if (defined $a->alert_type->in_renew && $U->is_true($a->alert_type->in_renew) && !$self->is_renewal);
706 next if (defined $a->alert_type->in_renew && !$U->is_true($a->alert_type->in_renew) && $self->is_renewal);
709 # filter on suppression
710 next if (grep { $a->alert_type->id == $_->alert_type} @$suppressions);
712 # filter on "only at circ lib"
713 if (defined $a->alert_type->at_circ) {
714 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
715 $self->copy->circ_lib->id : $self->copy->circ_lib;
716 my $orgs = $U->get_org_descendants($copy_circ_lib);
718 if ($U->is_true($a->alert_type->invert_location)) {
719 next if (grep {$_ == $self->circ_lib} @$orgs);
721 next unless (grep {$_ == $self->circ_lib} @$orgs);
725 # filter on "only at owning lib"
726 if (defined $a->alert_type->at_owning) {
727 my $copy_owning_lib = (ref $self->volume->owning_lib) ?
728 $self->volume->owning_lib->id : $self->volume->owning_lib;
729 my $orgs = $U->get_org_descendants($copy_owning_lib);
731 if ($U->is_true($a->alert_type->invert_location)) {
732 next if (grep {$_ == $self->circ_lib} @$orgs);
734 next unless (grep {$_ == $self->circ_lib} @$orgs);
738 $a->alert_type->next_status([$U->unique_unnested_numbers($a->alert_type->next_status)]);
740 push @final_alerts, $a;
743 $self->user_copy_alerts(\@final_alerts);
747 sub generate_system_copy_alerts {
749 return unless($self->copy);
751 # don't create system copy alerts if the copy
752 # is in a normal state; we're assuming that there's
753 # never a need to generate a popup for each and every
754 # checkin or checkout of normal items. If this assumption
755 # proves false, then we'll need to add a way to explicitly specify
756 # that a copy alert type should never generate a system copy alert
757 return if $self->copy_state eq 'NORMAL';
759 my $e = $self->editor;
761 my $suppress_orgs = $U->get_org_full_path($self->circ_lib);
762 my $suppressions = $e->search_actor_copy_alert_suppress(
763 {org => $suppress_orgs}
766 # events we care about ...
768 push(@$event, 'CHECKIN') if $self->is_checkin;
769 push(@$event, 'CHECKOUT') if $self->is_checkout;
770 return unless scalar(@$event);
772 my $alert_orgs = $U->get_org_ancestors($self->circ_lib);
773 my $alert_types = $e->search_config_copy_alert_type({
775 scope_org => $alert_orgs,
777 state => $self->copy_state,
778 '-or' => [ { in_renew => $self->is_renewal }, { in_renew => undef } ],
782 foreach my $a (@$alert_types) {
783 # filter on "only at circ lib"
784 if (defined $a->at_circ) {
785 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
786 $self->copy->circ_lib->id : $self->copy->circ_lib;
787 my $orgs = $U->get_org_descendants($copy_circ_lib);
789 if ($U->is_true($a->invert_location)) {
790 next if (grep {$_ == $self->circ_lib} @$orgs);
792 next unless (grep {$_ == $self->circ_lib} @$orgs);
796 # filter on "only at owning lib"
797 if (defined $a->at_owning) {
798 my $copy_owning_lib = (ref $self->volume->owning_lib) ?
799 $self->volume->owning_lib->id : $self->volume->owning_lib;
800 my $orgs = $U->get_org_descendants($copy_owning_lib);
802 if ($U->is_true($a->invert_location)) {
803 next if (grep {$_ == $self->circ_lib} @$orgs);
805 next unless (grep {$_ == $self->circ_lib} @$orgs);
809 push @final_types, $a;
813 $logger->info("circulator: found " . scalar(@final_types) . " system alert types for copy" .
819 # keep track of conditions corresponding to suppressed
820 # system alerts, as these may be used to overridee
821 # certain old-style-events
822 my %auto_override_conditions = ();
823 foreach my $t (@final_types) {
824 if ($t->next_status) {
825 if (grep { $t->id == $_->alert_type } @$suppressions) {
828 $t->next_status([$U->unique_unnested_numbers($t->next_status)]);
832 my $alert = new Fieldmapper::asset::copy_alert ();
833 $alert->alert_type($t->id);
834 $alert->copy($self->copy->id);
836 $alert->create_staff($e->requestor->id);
837 $alert->create_time('now');
838 $alert->ack_staff($e->requestor->id);
839 $alert->ack_time('now');
841 $alert = $e->create_asset_copy_alert($alert);
845 $alert->alert_type($t->clone);
847 push(@{$self->next_copy_status}, @{$t->next_status}) if ($t->next_status);
848 if (grep {$_->alert_type == $t->id} @$suppressions) {
849 $auto_override_conditions{join("\t", $t->state, $t->event)} = 1;
851 push(@alerts, $alert) unless (grep {$_->alert_type == $t->id} @$suppressions);
854 $self->system_copy_alerts(\@alerts);
855 $self->overrides_per_copy_alerts(\%auto_override_conditions);
858 sub add_overrides_from_system_copy_alerts {
860 my $e = $self->editor;
862 foreach my $condition (keys %{$self->overrides_per_copy_alerts()}) {
863 if (exists $COPY_ALERT_OVERRIDES{$condition}) {
865 push @{$self->override_args->{events}}, @{ $COPY_ALERT_OVERRIDES{$condition} };
866 # special handling for long-overdue and lost checkouts
867 if (grep { $_ eq 'OPEN_CIRCULATION_EXISTS' } @{ $COPY_ALERT_OVERRIDES{$condition} }) {
868 my $state = (split /\t/, $condition, -1)[0];
870 if ($state eq 'LOST' or $state eq 'LOST_AND_PAID') {
871 $setting = 'circ.copy_alerts.forgive_fines_on_lost_checkin';
872 } elsif ($state eq 'LONGOVERDUE') {
873 $setting = 'circ.copy_alerts.forgive_fines_on_long_overdue_checkin';
877 my $forgive = $U->ou_ancestor_setting_value(
878 $self->circ_lib, $setting, $e
880 if ($U->is_true($forgive)) {
881 $self->void_overdues(1);
883 $self->noop(1); # do not attempt transits, just check it in
892 my $e = $self->editor;
894 $self->next_copy_status([]) unless (defined $self->next_copy_status);
895 $self->overrides_per_copy_alerts({}) unless (defined $self->overrides_per_copy_alerts);
897 # --------------------------------------------------------------------------
898 # Grab the fleshed copy
899 # --------------------------------------------------------------------------
900 unless($self->is_noncat) {
903 $copy = $e->retrieve_asset_copy(
904 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
906 } elsif( $self->copy_barcode ) {
908 $copy = $e->search_asset_copy(
909 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
910 } elsif( $self->reservation ) {
911 my $res = $e->json_query(
913 "select" => {"acp" => ["id"]},
918 "field" => "barcode",
922 "field" => "current_resource"
931 "id" => (ref $self->reservation) ?
932 $self->reservation->id : $self->reservation
937 if (ref $res eq "ARRAY" and scalar @$res) {
938 $logger->info("circulator: mapped reservation " .
939 $self->reservation . " to copy " . $res->[0]->{"id"});
940 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
945 $self->save_trimmed_copy($copy);
950 {from => ['asset.copy_state', $copy->id]}
951 )->[0]{'asset.copy_state'}
954 $self->generate_system_copy_alerts;
955 $self->add_overrides_from_system_copy_alerts;
956 $self->collect_user_copy_alerts;
957 $self->filter_user_copy_alerts;
960 # We can't renew if there is no copy
961 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
962 if $self->is_renewal;
967 # --------------------------------------------------------------------------
969 # --------------------------------------------------------------------------
973 flesh_fields => {au => [ qw/ card / ]}
976 if( $self->patron_id ) {
977 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
978 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
980 } elsif( $self->patron_barcode ) {
982 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
983 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
984 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
986 $patron = $e->retrieve_actor_user($card->usr)
987 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
989 # Use the card we looked up, not the patron's primary, for card active checks
990 $patron->card($card);
993 if( my $copy = $self->copy ) {
996 $flesh->{flesh_fields}->{circ} = ['usr'];
998 my $circ = $e->search_action_circulation([
999 {target_copy => $copy->id, checkin_time => undef}, $flesh
1003 $patron = $circ->usr;
1004 $circ->usr($patron->id); # de-flesh for consistency
1010 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
1011 unless $self->patron($patron) or $self->is_checkin;
1013 unless($self->is_checkin) {
1015 # Check for inactivity and patron reg. expiration
1017 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
1018 unless $U->is_true($patron->active);
1020 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
1021 unless $U->is_true($patron->card->active);
1023 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
1024 clean_ISO8601($patron->expire_date));
1026 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
1027 if( CORE::time > $expire->epoch ) ;
1032 # --------------------------------------------------------------------------
1033 # Does the circ permit work
1034 # --------------------------------------------------------------------------
1038 $self->log_me("do_permit()");
1040 unless( $self->editor->requestor->id == $self->patron->id ) {
1041 return $self->bail_on_events($self->editor->event)
1042 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
1045 $self->check_captured_holds();
1046 $self->do_copy_checks();
1047 return if $self->bail_out;
1048 $self->run_patron_permit_scripts();
1049 $self->run_copy_permit_scripts()
1050 unless $self->is_precat or $self->is_noncat;
1051 $self->check_item_deposit_events();
1052 $self->override_events();
1053 return if $self->bail_out;
1055 if($self->is_precat and not $self->request_precat) {
1057 OpenILS::Event->new(
1058 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
1059 return $self->bail_out(1) unless $self->is_renewal;
1063 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
1066 sub check_item_deposit_events {
1068 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
1069 if $self->is_deposit and not $self->is_deposit_exempt;
1070 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
1071 if $self->is_rental and not $self->is_rental_exempt;
1074 # returns true if the user is not required to pay deposits
1075 sub is_deposit_exempt {
1077 my $pid = (ref $self->patron->profile) ?
1078 $self->patron->profile->id : $self->patron->profile;
1079 my $groups = $U->ou_ancestor_setting_value(
1080 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
1081 for my $grp (@$groups) {
1082 return 1 if $self->is_group_descendant($grp, $pid);
1087 # returns true if the user is not required to pay rental fees
1088 sub is_rental_exempt {
1090 my $pid = (ref $self->patron->profile) ?
1091 $self->patron->profile->id : $self->patron->profile;
1092 my $groups = $U->ou_ancestor_setting_value(
1093 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
1094 for my $grp (@$groups) {
1095 return 1 if $self->is_group_descendant($grp, $pid);
1100 sub is_group_descendant {
1101 my($self, $p_id, $c_id) = @_;
1102 return 0 unless defined $p_id and defined $c_id;
1103 return 1 if $c_id == $p_id;
1104 while(my $grp = $user_groups{$c_id}) {
1105 $c_id = $grp->parent;
1106 return 0 unless defined $c_id;
1107 return 1 if $c_id == $p_id;
1112 sub check_captured_holds {
1114 my $copy = $self->copy;
1115 my $patron = $self->patron;
1117 return undef unless $copy;
1119 my $s = $U->copy_status($copy->status)->id;
1120 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1121 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
1123 # Item is on the holds shelf, make sure it's going to the right person
1124 my $hold = $self->editor->search_action_hold_request(
1127 current_copy => $copy->id ,
1128 capture_time => { '!=' => undef },
1129 cancel_time => undef,
1130 fulfillment_time => undef
1134 flesh_fields => { ahr => ['usr'] }
1139 if ($hold and $hold->usr->id == $patron->id) {
1140 $self->checkout_is_for_hold(1);
1144 my $holdau = $hold->usr;
1147 $payload->{patron_name} = $holdau->first_given_name . ' ' . $holdau->family_name;
1148 $payload->{patron_id} = $holdau->id;
1150 $payload->{patron_name} = "???";
1152 $payload->{hold_id} = $hold->id;
1153 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF',
1154 payload => $payload));
1157 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1162 sub do_copy_checks {
1164 my $copy = $self->copy;
1165 return unless $copy;
1167 my $stat = $U->copy_status($copy->status)->id;
1169 # We cannot check out a copy if it is in-transit
1170 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1171 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1174 $self->handle_claims_returned();
1175 return if $self->bail_out;
1177 # no claims returned circ was found, check if there is any open circ
1178 unless( $self->is_renewal ) {
1180 my $circs = $self->editor->search_action_circulation(
1181 { target_copy => $copy->id, checkin_time => undef }
1184 if(my $old_circ = $circs->[0]) { # an open circ was found
1186 my $payload = {copy => $copy};
1188 if($old_circ->usr == $self->patron->id) {
1190 $payload->{old_circ} = $old_circ;
1192 # If there is an open circulation on the checkout item and an auto-renew
1193 # interval is defined, inform the caller that they should go
1194 # ahead and renew the item instead of warning about open circulations.
1196 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1198 'circ.checkout_auto_renew_age',
1202 if($auto_renew_intvl) {
1203 my $intvl_seconds = OpenILS::Utils::DateTime->interval_to_seconds($auto_renew_intvl);
1204 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clean_ISO8601($old_circ->xact_start) );
1206 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1207 $payload->{auto_renew} = 1;
1212 return $self->bail_on_events(
1213 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1219 my $LEGACY_CIRC_EVENT_MAP = {
1220 'no_item' => 'ITEM_NOT_CATALOGED',
1221 'actor.usr.barred' => 'PATRON_BARRED',
1222 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1223 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1224 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1225 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1226 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1227 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1228 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1229 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1230 'config.circ_matrix_test.total_copy_hold_ratio' =>
1231 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
1232 'config.circ_matrix_test.available_copy_hold_ratio' =>
1233 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
1237 # ---------------------------------------------------------------------
1238 # This pushes any patron-related events into the list but does not
1239 # set bail_out for any events
1240 # ---------------------------------------------------------------------
1241 sub run_patron_permit_scripts {
1243 my $patronid = $self->patron->id;
1248 my $results = $self->run_indb_circ_test;
1249 unless($self->circ_test_success) {
1250 my @trimmed_results;
1252 if ($self->is_noncat) {
1253 # no_item result is OK during noncat checkout
1254 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1258 if ($self->checkout_is_for_hold) {
1259 # if this checkout will fulfill a hold, ignore CIRC blocks
1260 # and rely instead on the (later-checked) FULFILL block
1262 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1263 my $fblock_pens = $self->editor->search_config_standing_penalty(
1264 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1266 for my $res (@$results) {
1267 my $name = $res->{fail_part} || '';
1268 next if grep {$_->name eq $name} @$fblock_pens;
1269 push(@trimmed_results, $res);
1273 # not for hold or noncat
1274 @trimmed_results = @$results;
1278 # update the final set of test results
1279 $self->matrix_test_result(\@trimmed_results);
1281 push @allevents, $self->matrix_test_result_events;
1285 $_->{payload} = $self->copy if
1286 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1289 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1291 $self->push_events(@allevents);
1294 sub matrix_test_result_codes {
1296 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1299 sub matrix_test_result_events {
1302 my $event = new OpenILS::Event(
1303 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1305 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1307 } (@{$self->matrix_test_result});
1310 sub run_indb_circ_test {
1312 return $self->matrix_test_result if $self->matrix_test_result;
1314 my $dbfunc = ($self->is_renewal) ?
1315 'action.item_user_renew_test' : 'action.item_user_circ_test';
1317 if( $self->is_precat && $self->request_precat) {
1318 $self->make_precat_copy;
1319 return if $self->bail_out;
1322 my $results = $self->editor->json_query(
1326 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1332 $self->circ_test_success($U->is_true($results->[0]->{success}));
1334 if(my $mp = $results->[0]->{matchpoint}) {
1335 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1336 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1337 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1338 if(defined($results->[0]->{renewals})) {
1339 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1341 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1342 if(defined($results->[0]->{grace_period})) {
1343 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1345 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1346 if(defined($results->[0]->{hard_due_date})) {
1347 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1349 # Grab the *last* response for limit_groups, where it is more likely to be filled
1350 $self->limit_groups($results->[-1]->{limit_groups});
1353 return $self->matrix_test_result($results);
1356 # ---------------------------------------------------------------------
1357 # given a use and copy, this will calculate the circulation policy
1358 # parameters. Only works with in-db circ.
1359 # ---------------------------------------------------------------------
1363 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1365 $self->run_indb_circ_test;
1368 circ_test_success => $self->circ_test_success,
1369 failure_events => [],
1370 failure_codes => [],
1371 matchpoint => $self->circ_matrix_matchpoint
1374 unless($self->circ_test_success) {
1375 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1376 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1379 if($self->circ_matrix_matchpoint) {
1380 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1381 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1382 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1383 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1385 my $policy = $self->get_circ_policy(
1386 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1388 $$results{$_} = $$policy{$_} for keys %$policy;
1394 # ---------------------------------------------------------------------
1395 # Loads the circ policy info for duration, recurring fine, and max
1396 # fine based on the current copy
1397 # ---------------------------------------------------------------------
1398 sub get_circ_policy {
1399 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1402 duration_rule => $duration_rule->name,
1403 recurring_fine_rule => $recurring_fine_rule->name,
1404 max_fine_rule => $max_fine_rule->name,
1405 max_fine => $self->get_max_fine_amount($max_fine_rule),
1406 fine_interval => $recurring_fine_rule->recurrence_interval,
1407 renewal_remaining => $duration_rule->max_renewals,
1408 auto_renewal_remaining => $duration_rule->max_auto_renewals,
1409 grace_period => $recurring_fine_rule->grace_period
1412 if($hard_due_date) {
1413 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1414 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1417 $policy->{duration_date_ceiling} = undef;
1418 $policy->{duration_date_ceiling_force} = undef;
1421 $policy->{duration} = $duration_rule->shrt
1422 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1423 $policy->{duration} = $duration_rule->normal
1424 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1425 $policy->{duration} = $duration_rule->extended
1426 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1428 $policy->{recurring_fine} = $recurring_fine_rule->low
1429 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1430 $policy->{recurring_fine} = $recurring_fine_rule->normal
1431 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1432 $policy->{recurring_fine} = $recurring_fine_rule->high
1433 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1438 sub get_max_fine_amount {
1440 my $max_fine_rule = shift;
1441 my $max_amount = $max_fine_rule->amount;
1443 # if is_percent is true then the max->amount is
1444 # use as a percentage of the copy price
1445 if ($U->is_true($max_fine_rule->is_percent)) {
1446 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1447 $max_amount = $price * $max_fine_rule->amount / 100;
1449 $U->ou_ancestor_setting_value(
1451 'circ.max_fine.cap_at_price',
1455 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1456 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1464 sub run_copy_permit_scripts {
1466 my $copy = $self->copy || return;
1470 my $results = $self->run_indb_circ_test;
1471 push @allevents, $self->matrix_test_result_events
1472 unless $self->circ_test_success;
1474 # See if this copy has an alert message
1475 my $ae = $self->check_copy_alert();
1476 push( @allevents, $ae ) if $ae;
1478 # uniquify the events
1479 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1480 @allevents = values %hash;
1482 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1484 $self->push_events(@allevents);
1488 sub check_copy_alert {
1491 if ($self->new_copy_alerts) {
1493 push @alerts, @{$self->user_copy_alerts} # we have preexisting alerts
1494 if ($self->user_copy_alerts && @{$self->user_copy_alerts});
1496 push @alerts, @{$self->system_copy_alerts} # we have new dynamic alerts
1497 if ($self->system_copy_alerts && @{$self->system_copy_alerts});
1500 $self->bail_out(1) if (!$self->override);
1501 return OpenILS::Event->new( 'COPY_ALERT_MESSAGE', payload => \@alerts);
1505 return undef if $self->is_renewal;
1506 return OpenILS::Event->new(
1507 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1508 if $self->copy and $self->copy->alert_message;
1514 # --------------------------------------------------------------------------
1515 # If the call is overriding and has permissions to override every collected
1516 # event, the are cleared. Any event that the caller does not have
1517 # permission to override, will be left in the event list and bail_out will
1519 # XXX We need code in here to cancel any holds/transits on copies
1520 # that are being force-checked out
1521 # --------------------------------------------------------------------------
1522 sub override_events {
1524 my @events = @{$self->events};
1525 return unless @events;
1526 my $oargs = $self->override_args;
1528 if(!$self->override) {
1529 return $self->bail_out(1)
1530 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1535 for my $e (@events) {
1536 my $tc = $e->{textcode};
1537 next if $tc eq 'SUCCESS';
1538 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1539 my $ov = "$tc.override";
1540 $logger->info("circulator: attempting to override event: $ov");
1542 return $self->bail_on_events($self->editor->event)
1543 unless( $self->editor->allowed($ov) );
1545 return $self->bail_out(1);
1551 # --------------------------------------------------------------------------
1552 # If there is an open claimsreturn circ on the requested copy, close the
1553 # circ if overriding, otherwise bail out
1554 # --------------------------------------------------------------------------
1555 sub handle_claims_returned {
1557 my $copy = $self->copy;
1559 my $CR = $self->editor->search_action_circulation(
1561 target_copy => $copy->id,
1562 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1563 checkin_time => undef,
1567 return unless ($CR = $CR->[0]);
1571 # - If the caller has set the override flag, we will check the item in
1572 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1574 $CR->checkin_time('now');
1575 $CR->checkin_scan_time('now');
1576 $CR->checkin_lib($self->circ_lib);
1577 $CR->checkin_workstation($self->editor->requestor->wsid);
1578 $CR->checkin_staff($self->editor->requestor->id);
1580 $evt = $self->editor->event
1581 unless $self->editor->update_action_circulation($CR);
1584 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1587 $self->bail_on_events($evt) if $evt;
1592 # --------------------------------------------------------------------------
1593 # This performs the checkout
1594 # --------------------------------------------------------------------------
1598 $self->log_me("do_checkout()");
1600 # make sure perms are good if this isn't a renewal
1601 unless( $self->is_renewal ) {
1602 return $self->bail_on_events($self->editor->event)
1603 unless( $self->editor->allowed('COPY_CHECKOUT') );
1606 # verify the permit key
1607 unless( $self->check_permit_key ) {
1608 if( $self->permit_override ) {
1609 return $self->bail_on_events($self->editor->event)
1610 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1612 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1616 # if this is a non-cataloged circ, build the circ and finish
1617 if( $self->is_noncat ) {
1618 $self->checkout_noncat;
1620 OpenILS::Event->new('SUCCESS',
1621 payload => { noncat_circ => $self->circ }));
1625 if( $self->is_precat ) {
1626 $self->make_precat_copy;
1627 return if $self->bail_out;
1629 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1630 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1633 $self->do_copy_checks;
1634 return if $self->bail_out;
1636 $self->run_checkout_scripts();
1637 return if $self->bail_out;
1639 $self->build_checkout_circ_object();
1640 return if $self->bail_out;
1642 my $modify_to_start = $self->booking_adjusted_due_date();
1643 return if $self->bail_out;
1645 $self->apply_modified_due_date($modify_to_start);
1646 return if $self->bail_out;
1648 return $self->bail_on_events($self->editor->event)
1649 unless $self->editor->create_action_circulation($self->circ);
1651 # refresh the circ to force local time zone for now
1652 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1654 if($self->limit_groups) {
1655 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1658 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1660 return if $self->bail_out;
1662 $self->apply_deposit_fee();
1663 return if $self->bail_out;
1665 $self->handle_checkout_holds();
1666 return if $self->bail_out;
1668 # ------------------------------------------------------------------------------
1669 # Update the patron penalty info in the DB. Run it for permit-overrides
1670 # since the penalties are not updated during the permit phase
1671 # ------------------------------------------------------------------------------
1672 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1674 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1677 if($self->is_renewal) {
1678 # flesh the billing summary for the checked-in circ
1679 $pcirc = $self->editor->retrieve_action_circulation([
1681 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1686 OpenILS::Event->new('SUCCESS',
1688 copy => $U->unflesh_copy($self->copy),
1689 volume => $self->volume,
1690 circ => $self->circ,
1692 holds_fulfilled => $self->fulfilled_holds,
1693 deposit_billing => $self->deposit_billing,
1694 rental_billing => $self->rental_billing,
1695 parent_circ => $pcirc,
1696 patron => ($self->return_patron) ? $self->patron : undef,
1697 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1703 sub apply_deposit_fee {
1705 my $copy = $self->copy;
1707 ($self->is_deposit and not $self->is_deposit_exempt) or
1708 ($self->is_rental and not $self->is_rental_exempt);
1710 return if $self->is_deposit and $self->skip_deposit_fee;
1711 return if $self->is_rental and $self->skip_rental_fee;
1713 my $bill = Fieldmapper::money::billing->new;
1714 my $amount = $copy->deposit_amount;
1718 if($self->is_deposit) {
1719 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1721 $self->deposit_billing($bill);
1723 $billing_type = OILS_BILLING_TYPE_RENTAL;
1725 $self->rental_billing($bill);
1728 $bill->xact($self->circ->id);
1729 $bill->amount($amount);
1730 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1731 $bill->billing_type($billing_type);
1732 $bill->btype($btype);
1733 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1735 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1740 my $copy = $self->copy;
1742 my $stat = $copy->status if ref $copy->status;
1743 my $loc = $copy->location if ref $copy->location;
1744 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1746 $copy->status($stat->id) if $stat;
1747 $copy->location($loc->id) if $loc;
1748 $copy->circ_lib($circ_lib->id) if $circ_lib;
1749 $copy->editor($self->editor->requestor->id);
1750 $copy->edit_date('now');
1751 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1753 return $self->bail_on_events($self->editor->event)
1754 unless $self->editor->update_asset_copy($self->copy);
1756 $copy->status($U->copy_status($copy->status));
1757 $copy->location($loc) if $loc;
1758 $copy->circ_lib($circ_lib) if $circ_lib;
1761 sub update_reservation {
1763 my $reservation = $self->reservation;
1765 my $usr = $reservation->usr;
1766 my $target_rt = $reservation->target_resource_type;
1767 my $target_r = $reservation->target_resource;
1768 my $current_r = $reservation->current_resource;
1770 $reservation->usr($usr->id) if ref $usr;
1771 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1772 $reservation->target_resource($target_r->id) if ref $target_r;
1773 $reservation->current_resource($current_r->id) if ref $current_r;
1775 return $self->bail_on_events($self->editor->event)
1776 unless $self->editor->update_booking_reservation($self->reservation);
1779 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1780 $self->reservation($reservation);
1784 sub bail_on_events {
1785 my( $self, @evts ) = @_;
1786 $self->push_events(@evts);
1790 # ------------------------------------------------------------------------------
1791 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1792 # affects copies that will fulfill holds and CIRC affects all other copies.
1793 # If blocks exists, bail, push Events onto the event pile, and return true.
1794 # ------------------------------------------------------------------------------
1795 sub check_hold_fulfill_blocks {
1798 # With the addition of ignore_proximity in csp, we need to fetch
1799 # the proximity of both the circ_lib and the copy's circ_lib to
1800 # the patron's home_ou.
1801 my ($ou_prox, $copy_prox);
1802 my $home_ou = (ref($self->patron->home_ou)) ? $self->patron->home_ou->id : $self->patron->home_ou;
1803 $ou_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $self->circ_lib);
1804 $ou_prox = -1 unless (defined($ou_prox));
1805 my $copy_ou = (ref($self->copy->circ_lib)) ? $self->copy->circ_lib->id : $self->copy->circ_lib;
1806 if ($copy_ou == $self->circ_lib) {
1807 # Save us the time of an extra query.
1808 $copy_prox = $ou_prox;
1810 $copy_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $copy_ou);
1811 $copy_prox = -1 unless (defined($copy_prox));
1814 # See if the user has any penalties applied that prevent hold fulfillment
1815 my $pens = $self->editor->json_query({
1816 select => {csp => ['name', 'label']},
1817 from => {ausp => {csp => {}}},
1820 usr => $self->patron->id,
1821 org_unit => $U->get_org_full_path($self->circ_lib),
1823 {stop_date => undef},
1824 {stop_date => {'>' => 'now'}}
1828 block_list => {'like' => '%FULFILL%'},
1830 {ignore_proximity => undef},
1831 {ignore_proximity => {'<' => $ou_prox}},
1832 {ignore_proximity => {'<' => $copy_prox}}
1838 return 0 unless @$pens;
1840 for my $pen (@$pens) {
1841 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1842 my $event = OpenILS::Event->new($pen->{name});
1843 $event->{desc} = $pen->{label};
1844 $self->push_events($event);
1847 $self->override_events;
1848 return $self->bail_out;
1852 # ------------------------------------------------------------------------------
1853 # When an item is checked out, see if we can fulfill a hold for this patron
1854 # ------------------------------------------------------------------------------
1855 sub handle_checkout_holds {
1857 my $copy = $self->copy;
1858 my $patron = $self->patron;
1860 my $e = $self->editor;
1861 $self->fulfilled_holds([]);
1863 # non-cats can't fulfill a hold
1864 return if $self->is_noncat;
1866 my $hold = $e->search_action_hold_request({
1867 current_copy => $copy->id ,
1868 cancel_time => undef,
1869 fulfillment_time => undef
1872 if($hold and $hold->usr != $patron->id) {
1873 # reset the hold since the copy is now checked out
1875 $logger->info("circulator: un-targeting hold ".$hold->id.
1876 " because copy ".$copy->id." is getting checked out");
1878 $hold->clear_prev_check_time;
1879 $hold->clear_current_copy;
1880 $hold->clear_capture_time;
1881 $hold->clear_shelf_time;
1882 $hold->clear_shelf_expire_time;
1883 $hold->clear_current_shelf_lib;
1885 return $self->bail_on_event($e->event)
1886 unless $e->update_action_hold_request($hold);
1892 $hold = $self->find_related_user_hold($copy, $patron) or return;
1893 $logger->info("circulator: found related hold to fulfill in checkout");
1896 return if $self->check_hold_fulfill_blocks;
1898 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1900 # if the hold was never officially captured, capture it.
1901 $hold->current_copy($copy->id);
1902 $hold->capture_time('now') unless $hold->capture_time;
1903 $hold->fulfillment_time('now');
1904 $hold->fulfillment_staff($e->requestor->id);
1905 $hold->fulfillment_lib($self->circ_lib);
1907 return $self->bail_on_events($e->event)
1908 unless $e->update_action_hold_request($hold);
1910 return $self->fulfilled_holds([$hold->id]);
1914 # ------------------------------------------------------------------------------
1915 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1916 # the patron directly targets the checked out item, see if there is another hold
1917 # for the patron that could be fulfilled by the checked out item. Fulfill the
1918 # oldest hold and only fulfill 1 of them.
1920 # For "another hold":
1922 # First, check for one that the copy matches via hold_copy_map, ensuring that
1923 # *any* hold type that this copy could fill may end up filled.
1925 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1926 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1927 # that are non-requestable to count as capturing those hold types.
1928 # ------------------------------------------------------------------------------
1929 sub find_related_user_hold {
1930 my($self, $copy, $patron) = @_;
1931 my $e = $self->editor;
1933 # holds on precat copies are always copy-level, so this call will
1934 # always return undef. Exit early.
1935 return undef if $self->is_precat;
1937 return undef unless $U->ou_ancestor_setting_value(
1938 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1940 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1942 select => {ahr => ['id']},
1951 fkey => 'current_copy',
1952 type => 'left' # there may be no current_copy
1959 fulfillment_time => undef,
1960 cancel_time => undef,
1962 {expire_time => undef},
1963 {expire_time => {'>' => 'now'}}
1967 target_copy => $self->copy->id
1971 {id => undef}, # left-join copy may be nonexistent
1972 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1976 order_by => {ahr => {request_time => {direction => 'asc'}}},
1980 my $hold_info = $e->json_query($args)->[0];
1981 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1982 return undef if $U->ou_ancestor_setting_value(
1983 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1985 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1987 select => {ahr => ['id']},
1992 fkey => 'current_copy',
1993 type => 'left' # there may be no current_copy
2000 fulfillment_time => undef,
2001 cancel_time => undef,
2003 {expire_time => undef},
2004 {expire_time => {'>' => 'now'}}
2011 target => $self->volume->id
2017 target => $self->title->id
2023 {id => undef}, # left-join copy may be nonexistent
2024 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
2028 order_by => {ahr => {request_time => {direction => 'asc'}}},
2032 $hold_info = $e->json_query($args)->[0];
2033 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
2038 sub run_checkout_scripts {
2051 my $hard_due_date_name;
2053 $self->run_indb_circ_test();
2054 $duration = $self->circ_matrix_matchpoint->duration_rule;
2055 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
2056 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
2057 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
2059 $duration_name = $duration->name if $duration;
2060 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
2063 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
2064 return $self->bail_on_events($evt) if ($evt && !$nobail);
2066 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
2067 return $self->bail_on_events($evt) if ($evt && !$nobail);
2069 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
2070 return $self->bail_on_events($evt) if ($evt && !$nobail);
2072 if($hard_due_date_name) {
2073 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
2074 return $self->bail_on_events($evt) if ($evt && !$nobail);
2080 # The item circulates with an unlimited duration
2084 $hard_due_date = undef;
2087 $self->duration_rule($duration);
2088 $self->recurring_fines_rule($recurring);
2089 $self->max_fine_rule($max_fine);
2090 $self->hard_due_date($hard_due_date);
2094 sub build_checkout_circ_object {
2097 my $circ = Fieldmapper::action::circulation->new;
2098 my $duration = $self->duration_rule;
2099 my $max = $self->max_fine_rule;
2100 my $recurring = $self->recurring_fines_rule;
2101 my $hard_due_date = $self->hard_due_date;
2102 my $copy = $self->copy;
2103 my $patron = $self->patron;
2104 my $duration_date_ceiling;
2105 my $duration_date_ceiling_force;
2109 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
2110 $duration_date_ceiling = $policy->{duration_date_ceiling};
2111 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
2113 my $dname = $duration->name;
2114 my $mname = $max->name;
2115 my $rname = $recurring->name;
2117 if($hard_due_date) {
2118 $hdname = $hard_due_date->name;
2121 $logger->debug("circulator: building circulation ".
2122 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
2124 $circ->duration($policy->{duration});
2125 $circ->recurring_fine($policy->{recurring_fine});
2126 $circ->duration_rule($duration->name);
2127 $circ->recurring_fine_rule($recurring->name);
2128 $circ->max_fine_rule($max->name);
2129 $circ->max_fine($policy->{max_fine});
2130 $circ->fine_interval($recurring->recurrence_interval);
2131 $circ->renewal_remaining($duration->max_renewals);
2132 $circ->auto_renewal_remaining($duration->max_auto_renewals);
2133 $circ->grace_period($policy->{grace_period});
2137 $logger->info("circulator: copy found with an unlimited circ duration");
2138 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2139 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2140 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2141 $circ->renewal_remaining(0);
2142 $circ->grace_period(0);
2145 $circ->target_copy( $copy->id );
2146 $circ->usr( $patron->id );
2147 $circ->circ_lib( $self->circ_lib );
2148 $circ->workstation($self->editor->requestor->wsid)
2149 if defined $self->editor->requestor->wsid;
2151 # renewals maintain a link to the parent circulation
2152 $circ->parent_circ($self->parent_circ);
2154 if( $self->is_renewal ) {
2155 $circ->opac_renewal('t') if $self->opac_renewal;
2156 $circ->phone_renewal('t') if $self->phone_renewal;
2157 $circ->desk_renewal('t') if $self->desk_renewal;
2158 $circ->renewal_remaining($self->renewal_remaining);
2159 $circ->circ_staff($self->editor->requestor->id);
2162 if ( $self->is_autorenewal ){
2163 $circ->auto_renewal_remaining($self->auto_renewal_remaining);
2164 $circ->auto_renewal('t');
2167 # if the user provided an overiding checkout time,
2168 # (e.g. the checkout really happened several hours ago), then
2169 # we apply that here. Does this need a perm??
2170 $circ->xact_start(clean_ISO8601($self->checkout_time))
2171 if $self->checkout_time;
2173 # if a patron is renewing, 'requestor' will be the patron
2174 $circ->circ_staff($self->editor->requestor->id);
2175 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2180 sub do_reservation_pickup {
2183 $self->log_me("do_reservation_pickup()");
2185 $self->reservation->pickup_time('now');
2188 $self->reservation->current_resource &&
2189 $U->is_true($self->reservation->target_resource_type->catalog_item)
2191 # We used to try to set $self->copy and $self->patron here,
2192 # but that should already be done.
2194 $self->run_checkout_scripts(1);
2196 my $duration = $self->duration_rule;
2197 my $max = $self->max_fine_rule;
2198 my $recurring = $self->recurring_fines_rule;
2200 if ($duration && $max && $recurring) {
2201 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2203 my $dname = $duration->name;
2204 my $mname = $max->name;
2205 my $rname = $recurring->name;
2207 $logger->debug("circulator: updating reservation ".
2208 "with duration=$dname, maxfine=$mname, recurring=$rname");
2210 $self->reservation->fine_amount($policy->{recurring_fine});
2211 $self->reservation->max_fine($policy->{max_fine});
2212 $self->reservation->fine_interval($recurring->recurrence_interval);
2215 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2216 $self->update_copy();
2219 $self->reservation->fine_amount(
2220 $self->reservation->target_resource_type->fine_amount
2222 $self->reservation->max_fine(
2223 $self->reservation->target_resource_type->max_fine
2225 $self->reservation->fine_interval(
2226 $self->reservation->target_resource_type->fine_interval
2230 $self->update_reservation();
2233 sub do_reservation_return {
2235 my $request = shift;
2237 $self->log_me("do_reservation_return()");
2239 if (not ref $self->reservation) {
2240 my ($reservation, $evt) =
2241 $U->fetch_booking_reservation($self->reservation);
2242 return $self->bail_on_events($evt) if $evt;
2243 $self->reservation($reservation);
2246 $self->handle_fines(1);
2247 $self->reservation->return_time('now');
2248 $self->update_reservation();
2249 $self->reshelve_copy if $self->copy;
2251 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2252 $self->copy( $self->reservation->current_resource->catalog_item );
2256 sub booking_adjusted_due_date {
2258 my $circ = $self->circ;
2259 my $copy = $self->copy;
2261 return undef unless $self->use_booking;
2265 if( $self->due_date ) {
2267 return $self->bail_on_events($self->editor->event)
2268 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2270 $circ->due_date(clean_ISO8601($self->due_date));
2274 return unless $copy and $circ->due_date;
2277 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2278 if (@$booking_items) {
2279 my $booking_item = $booking_items->[0];
2280 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2282 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2283 my $shorten_circ_setting = $resource_type->elbow_room ||
2284 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2287 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2288 my $bookings = $booking_ses->request('open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken, {
2289 resource => $booking_item->id
2290 , search_start => 'now'
2291 , search_end => $circ->due_date
2292 , fields => { cancel_time => undef, return_time => undef }
2294 $booking_ses->disconnect;
2296 throw OpenSRF::EX::ERROR ("Improper input arguments") unless defined $bookings;
2297 return $self->bail_on_events($bookings) if ref($bookings) eq 'HASH';
2299 my $dt_parser = DateTime::Format::ISO8601->new;
2300 my $due_date = $dt_parser->parse_datetime( clean_ISO8601($circ->due_date) );
2302 for my $bid (@$bookings) {
2304 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2306 my $booking_start = $dt_parser->parse_datetime( clean_ISO8601($booking->start_time) );
2307 my $booking_end = $dt_parser->parse_datetime( clean_ISO8601($booking->end_time) );
2309 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2310 if ($booking_start < DateTime->now);
2313 if ($U->is_true($stop_circ_setting)) {
2314 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2316 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2317 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2320 # We set the circ duration here only to affect the logic that will
2321 # later (in a DB trigger) mangle the time part of the due date to
2322 # 11:59pm. Having any circ duration that is not a whole number of
2323 # days is enough to prevent the "correction."
2324 my $new_circ_duration = $due_date->epoch - time;
2325 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2326 $circ->duration("$new_circ_duration seconds");
2328 $circ->due_date(clean_ISO8601($due_date->strftime('%FT%T%z')));
2332 return $self->bail_on_events($self->editor->event)
2333 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2339 sub apply_modified_due_date {
2341 my $shift_earlier = shift;
2342 my $circ = $self->circ;
2343 my $copy = $self->copy;
2345 if( $self->due_date ) {
2347 return $self->bail_on_events($self->editor->event)
2348 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2350 $circ->due_date(clean_ISO8601($self->due_date));
2354 # if the due_date lands on a day when the location is closed
2355 return unless $copy and $circ->due_date;
2357 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2359 # due-date overlap should be determined by the location the item
2360 # is checked out from, not the owning or circ lib of the item
2361 my $org = $self->circ_lib;
2363 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2364 " with an item due date of ".$circ->due_date );
2366 my $dateinfo = $U->storagereq(
2367 'open-ils.storage.actor.org_unit.closed_date.overlap',
2368 $org, $circ->due_date );
2371 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2372 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2374 # XXX make the behavior more dynamic
2375 # for now, we just push the due date to after the close date
2376 if ($shift_earlier) {
2377 $circ->due_date($dateinfo->{start});
2379 $circ->due_date($dateinfo->{end});
2387 sub create_due_date {
2388 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2390 # Look up circulating library's TZ, or else use client TZ, falling
2392 my $tz = $U->ou_ancestor_setting_value(
2398 my $due_date = $start_time ?
2399 DateTime::Format::ISO8601
2401 ->parse_datetime(clean_ISO8601($start_time))
2402 ->set_time_zone($tz) :
2403 DateTime->now(time_zone => $tz);
2405 # add the circ duration
2406 $due_date->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($duration, $due_date));
2409 my $cdate = DateTime::Format::ISO8601
2411 ->parse_datetime(clean_ISO8601($date_ceiling))
2412 ->set_time_zone($tz);
2414 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2415 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2420 # return ISO8601 time with timezone
2421 return $due_date->strftime('%FT%T%z');
2426 sub make_precat_copy {
2428 my $copy = $self->copy;
2429 return $self->bail_on_events(OpenILS::Event->new('PERM_FAILURE'))
2430 unless $self->editor->allowed('CREATE_PRECAT') || $self->is_renewal;
2433 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2435 $copy->editor($self->editor->requestor->id);
2436 $copy->edit_date('now');
2437 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2438 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2439 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2440 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2441 $self->update_copy();
2445 $logger->info("circulator: Creating a new precataloged ".
2446 "copy in checkout with barcode " . $self->copy_barcode);
2448 $copy = Fieldmapper::asset::copy->new;
2449 $copy->circ_lib($self->circ_lib);
2450 $copy->creator($self->editor->requestor->id);
2451 $copy->editor($self->editor->requestor->id);
2452 $copy->barcode($self->copy_barcode);
2453 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2454 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2455 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2457 $copy->dummy_title($self->dummy_title || "");
2458 $copy->dummy_author($self->dummy_author || "");
2459 $copy->dummy_isbn($self->dummy_isbn || "");
2460 $copy->circ_modifier($self->circ_modifier);
2463 # See if we need to override the circ_lib for the copy with a configured circ_lib
2464 # Setting is shortname of the org unit
2465 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2466 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2468 if($precat_circ_lib) {
2469 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2472 $self->bail_on_events($self->editor->event);
2476 $copy->circ_lib($org->id);
2480 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2482 $self->push_events($self->editor->event);
2488 sub checkout_noncat {
2494 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2495 my $count = $self->noncat_count || 1;
2496 my $cotime = clean_ISO8601($self->checkout_time) || "";
2498 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2502 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2503 $self->editor->requestor->id,
2511 $self->push_events($evt);
2519 # If a copy goes into transit and is then checked in before the transit checkin
2520 # interval has expired, push an event onto the overridable events list.
2521 sub check_transit_checkin_interval {
2524 # only concerned with in-transit items
2525 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2527 # no interval, no problem
2528 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2529 return unless $interval;
2531 # capture the transit so we don't have to fetch it again later during checkin
2533 $self->editor->search_action_transit_copy(
2534 {target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef}
2538 # transit from X to X for whatever reason has no min interval
2539 return if $self->transit->source == $self->transit->dest;
2541 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($interval);
2542 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($self->transit->source_send_time));
2543 my $horizon = $t_start->add(seconds => $seconds);
2545 # See if we are still within the transit checkin forbidden range
2546 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2547 if $horizon > DateTime->now;
2550 # Retarget local holds at checkin
2551 sub checkin_retarget {
2553 return unless $self->retarget_mode and $self->retarget_mode =~ m/retarget/; # Retargeting?
2554 return unless $self->is_checkin; # Renewals need not be checked
2555 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2556 return if $self->is_precat; # No holds for precats
2557 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2558 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2559 my $status = $U->copy_status($self->copy->status);
2560 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2561 # Specifically target items that are likely new (by status ID)
2562 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2563 my $location = $self->copy->location;
2564 if(!ref($location)) {
2565 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2566 $self->copy->location($location);
2568 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2570 # Fetch holds for the bib
2571 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2572 $self->editor->authtoken,
2575 capture_time => undef, # No touching captured holds
2576 frozen => 'f', # Don't bother with frozen holds
2577 pickup_lib => $self->circ_lib # Only holds actually here
2580 # Error? Skip the step.
2581 return if exists $result->{"ilsevent"};
2585 foreach my $holdlist (keys %{$result}) {
2586 push @$holds, @{$result->{$holdlist}};
2589 return if scalar(@$holds) == 0; # No holds, no retargeting
2591 # Check for parts on this copy
2592 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2593 my %parts_hash = ();
2594 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2596 # Loop over holds in request-ish order
2597 # Stage 1: Get them into request-ish order
2598 # Also grab type and target for skipping low hanging ones
2599 $result = $self->editor->json_query({
2600 "select" => { "ahr" => ["id", "hold_type", "target"] },
2601 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2602 "where" => { "id" => $holds },
2604 { "class" => "pgt", "field" => "hold_priority"},
2605 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2606 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2607 { "class" => "ahr", "field" => "request_time"}
2612 if (ref $result eq "ARRAY" and scalar @$result) {
2613 foreach (@{$result}) {
2614 # Copy level, but not this copy?
2615 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2616 and $_->{target} != $self->copy->id);
2617 # Volume level, but not this volume?
2618 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2619 if(@$parts) { # We have parts?
2621 next if ($_->{hold_type} eq 'T');
2622 # Skip part holds for parts not on this copy
2623 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2625 # No parts, no part holds
2626 next if ($_->{hold_type} eq 'P');
2628 # So much for easy stuff, attempt a retarget!
2629 my $tresult = $U->simplereq(
2630 'open-ils.hold-targeter',
2631 'open-ils.hold-targeter.target',
2632 {hold => $_->{id}, find_copy => $self->copy->id}
2634 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2635 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2643 $self->log_me("do_checkin()");
2645 return $self->bail_on_events(
2646 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2649 $self->check_transit_checkin_interval;
2650 $self->checkin_retarget;
2652 # the renew code and mk_env should have already found our circulation object
2653 unless( $self->circ ) {
2655 my $circs = $self->editor->search_action_circulation(
2656 { target_copy => $self->copy->id, checkin_time => undef });
2658 $self->circ($$circs[0]);
2660 # for now, just warn if there are multiple open circs on a copy
2661 $logger->warn("circulator: we have ".scalar(@$circs).
2662 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2665 my $stat = $U->copy_status($self->copy->status)->id;
2667 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2668 # differently if they are already paid for. We need to check for this
2669 # early since overdue generation is potentially affected.
2670 my $dont_change_lost_zero = 0;
2671 if ($stat == OILS_COPY_STATUS_LOST
2672 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2673 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2675 # LOST fine settings are controlled by the copy's circ lib, not the the
2677 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2678 $self->copy->circ_lib->id : $self->copy->circ_lib;
2679 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2680 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2681 $self->editor) || 0;
2683 # Don't assume there's always a circ based on copy status
2684 if ($dont_change_lost_zero && $self->circ) {
2685 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2686 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2689 $self->dont_change_lost_zero($dont_change_lost_zero);
2692 my $latest_inventory = Fieldmapper::asset::latest_inventory->new;
2694 if ($self->do_inventory_update) {
2695 $latest_inventory->inventory_date('now');
2696 $latest_inventory->inventory_workstation($self->editor->requestor->wsid);
2697 $latest_inventory->copy($self->copy->id());
2699 my $alci = $self->editor->search_asset_latest_inventory(
2700 {copy => $self->copy->id}
2702 $latest_inventory = $alci->[0]
2704 $self->latest_inventory($latest_inventory);
2706 if( $self->checkin_check_holds_shelf() ) {
2707 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2708 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2709 if($self->fake_hold_dest) {
2710 $self->hold->pickup_lib($self->circ_lib);
2712 $self->checkin_flesh_events;
2716 unless( $self->is_renewal ) {
2717 return $self->bail_on_events($self->editor->event)
2718 unless $self->editor->allowed('COPY_CHECKIN');
2721 $self->push_events($self->check_copy_alert());
2722 $self->push_events($self->check_checkin_copy_status());
2724 # if the circ is marked as 'claims returned', add the event to the list
2725 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2726 if ($self->circ and $self->circ->stop_fines
2727 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2729 $self->check_circ_deposit();
2731 # handle the overridable events
2732 $self->override_events unless $self->is_renewal;
2733 return if $self->bail_out;
2735 if( $self->copy and !$self->transit ) {
2737 $self->editor->search_action_transit_copy(
2738 { target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef }
2744 $self->checkin_handle_circ_start;
2745 return if $self->bail_out;
2747 if (!$dont_change_lost_zero) {
2748 # if this circ is LOST and we are configured to generate overdue
2749 # fines for lost items on checkin (to fill the gap between mark
2750 # lost time and when the fines would have naturally stopped), then
2751 # stop_fines is no longer valid and should be cleared.
2753 # stop_fines will be set again during the handle_fines() stage.
2754 # XXX should this setting come from the copy circ lib (like other
2755 # LOST settings), instead of the circulation circ lib?
2756 if ($stat == OILS_COPY_STATUS_LOST) {
2757 $self->circ->clear_stop_fines if
2758 $U->ou_ancestor_setting_value(
2760 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2765 # Set stop_fines when claimed never checked out
2766 $self->circ->stop_fines( OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT ) if( $self->claims_never_checked_out );
2768 # handle fines for this circ, including overdue gen if needed
2769 $self->handle_fines;
2772 $self->checkin_handle_circ_finish;
2773 return if $self->bail_out;
2774 $self->checkin_changed(1);
2776 } elsif( $self->transit ) {
2777 my $hold_transit = $self->process_received_transit;
2778 $self->checkin_changed(1);
2780 if( $self->bail_out ) {
2781 $self->checkin_flesh_events;
2785 if( my $e = $self->check_checkin_copy_status() ) {
2786 # If the original copy status is special, alert the caller
2787 my $ev = $self->events;
2788 $self->events([$e]);
2789 $self->override_events;
2790 return if $self->bail_out;
2794 if( $hold_transit or
2795 $U->copy_status($self->copy->status)->id
2796 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2799 if( $hold_transit ) {
2800 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2802 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2807 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2809 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2810 $self->reshelve_copy(1);
2811 $self->cancelled_hold_transit(1);
2812 $self->notify_hold(0); # don't notify for cancelled holds
2813 $self->fake_hold_dest(0);
2814 return if $self->bail_out;
2816 } elsif ($hold and $hold->hold_type eq 'R') {
2818 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2819 $self->notify_hold(0); # No need to notify
2820 $self->fake_hold_dest(0);
2821 $self->noop(1); # Don't try and capture for other holds/transits now
2822 $self->update_copy();
2823 $hold->fulfillment_time('now');
2824 $self->bail_on_events($self->editor->event)
2825 unless $self->editor->update_action_hold_request($hold);
2829 # hold transited to correct location
2830 if($self->fake_hold_dest) {
2831 $hold->pickup_lib($self->circ_lib);
2833 $self->checkin_flesh_events;
2838 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2840 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2841 " that is in-transit, but there is no transit.. repairing");
2842 $self->reshelve_copy(1);
2843 return if $self->bail_out;
2846 if( $self->is_renewal ) {
2847 $self->finish_fines_and_voiding;
2848 return if $self->bail_out;
2849 $self->push_events(OpenILS::Event->new('SUCCESS'));
2853 # ------------------------------------------------------------------------------
2854 # Circulations and transits are now closed where necessary. Now go on to see if
2855 # this copy can fulfill a hold or needs to be routed to a different location
2856 # ------------------------------------------------------------------------------
2858 my $needed_for_something = 0; # formerly "needed_for_hold"
2860 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2862 if (!$self->remote_hold) {
2863 if ($self->use_booking) {
2864 my $potential_hold = $self->hold_capture_is_possible;
2865 my $potential_reservation = $self->reservation_capture_is_possible;
2867 if ($potential_hold and $potential_reservation) {
2868 $logger->info("circulator: item could fulfill either hold or reservation");
2869 $self->push_events(new OpenILS::Event(
2870 "HOLD_RESERVATION_CONFLICT",
2871 "hold" => $potential_hold,
2872 "reservation" => $potential_reservation
2874 return if $self->bail_out;
2875 } elsif ($potential_hold) {
2876 $needed_for_something =
2877 $self->attempt_checkin_hold_capture;
2878 } elsif ($potential_reservation) {
2879 $needed_for_something =
2880 $self->attempt_checkin_reservation_capture;
2883 $needed_for_something = $self->attempt_checkin_hold_capture;
2886 return if $self->bail_out;
2888 unless($needed_for_something) {
2889 my $circ_lib = (ref $self->copy->circ_lib) ?
2890 $self->copy->circ_lib->id : $self->copy->circ_lib;
2892 if( $self->remote_hold ) {
2893 $circ_lib = $self->remote_hold->pickup_lib;
2894 $logger->warn("circulator: Copy ".$self->copy->barcode.
2895 " is on a remote hold's shelf, sending to $circ_lib");
2898 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2900 my $suppress_transit = 0;
2902 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2903 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2904 if($suppress_transit_source && $suppress_transit_source->{value}) {
2905 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2906 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2907 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2908 $suppress_transit = 1;
2913 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2914 # copy is where it needs to be, either for hold or reshelving
2916 $self->checkin_handle_precat();
2917 return if $self->bail_out;
2920 # copy needs to transit "home", or stick here if it's a floating copy
2922 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2923 my $res = $self->editor->json_query(
2925 'evergreen.can_float',
2926 $self->copy->floating->id,
2927 $self->copy->circ_lib,
2932 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2934 if ($can_float) { # Yep, floating, stick here
2935 $self->checkin_changed(1);
2936 $self->copy->circ_lib( $self->circ_lib );
2939 my $bc = $self->copy->barcode;
2940 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2941 $self->checkin_build_copy_transit($circ_lib);
2942 return if $self->bail_out;
2943 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2947 } else { # no-op checkin
2948 if ($self->copy->floating) { # XXX floating items still stick where they are even with no-op checkin?
2949 my $res = $self->editor->json_query(
2952 'evergreen.can_float',
2953 $self->copy->floating->id,
2954 $self->copy->circ_lib,
2959 if ($res && @$res && $U->is_true($res->[0]->{'evergreen.can_float'})) {
2960 $self->checkin_changed(1);
2961 $self->copy->circ_lib( $self->circ_lib );
2967 if($self->claims_never_checked_out and
2968 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2970 # the item was not supposed to be checked out to the user and should now be marked as missing
2971 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_MISSING;
2972 $self->copy->status($next_status);
2976 $self->reshelve_copy unless $needed_for_something;
2979 return if $self->bail_out;
2981 unless($self->checkin_changed) {
2983 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2984 my $stat = $U->copy_status($self->copy->status)->id;
2986 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2987 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2988 $self->bail_out(1); # no need to commit anything
2992 $self->push_events(OpenILS::Event->new('SUCCESS'))
2993 unless @{$self->events};
2996 $self->finish_fines_and_voiding;
2998 OpenILS::Utils::Penalty->calculate_penalties(
2999 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
3001 $self->checkin_flesh_events;
3005 sub finish_fines_and_voiding {
3007 return unless $self->circ;
3009 return unless $self->backdate or $self->void_overdues;
3011 # void overdues after fine generation to prevent concurrent DB access to overdue billings
3012 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
3014 my $evt = $CC->void_or_zero_overdues(
3015 $self->editor, $self->circ, {backdate => $self->void_overdues ? undef : $self->backdate, note => $note});
3017 return $self->bail_on_events($evt) if $evt;
3019 # Make sure the circ is open or closed as necessary.
3020 $evt = $U->check_open_xact($self->editor, $self->circ->id);
3021 return $self->bail_on_events($evt) if $evt;
3027 # if a deposit was payed for this item, push the event
3028 sub check_circ_deposit {
3030 return unless $self->circ;
3031 my $deposit = $self->editor->search_money_billing(
3033 xact => $self->circ->id,
3035 }, {idlist => 1})->[0];
3037 $self->push_events(OpenILS::Event->new(
3038 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
3043 my $force = $self->force || shift;
3044 my $copy = $self->copy;
3046 my $stat = $U->copy_status($copy->status)->id;
3048 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3051 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
3052 $stat != OILS_COPY_STATUS_CATALOGING and
3053 $stat != OILS_COPY_STATUS_IN_TRANSIT and
3054 $stat != $next_status )) {
3056 $copy->status( $next_status );
3058 $self->checkin_changed(1);
3063 # Returns true if the item is at the current location
3064 # because it was transited there for a hold and the
3065 # hold has not been fulfilled
3066 sub checkin_check_holds_shelf {
3068 return 0 unless $self->copy;
3071 $U->copy_status($self->copy->status)->id ==
3072 OILS_COPY_STATUS_ON_HOLDS_SHELF;
3074 # Attempt to clear shelf expired holds for this copy
3075 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
3076 if($self->clear_expired);
3078 # find the hold that put us on the holds shelf
3079 my $holds = $self->editor->search_action_hold_request(
3081 current_copy => $self->copy->id,
3082 capture_time => { '!=' => undef },
3083 fulfillment_time => undef,
3084 cancel_time => undef,
3089 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
3090 $self->reshelve_copy(1);
3094 my $hold = $$holds[0];
3096 $logger->info("circulator: we found a captured, un-fulfilled hold [".
3097 $hold->id. "] for copy ".$self->copy->barcode);
3099 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3100 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3101 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3102 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3103 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3104 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
3105 $self->fake_hold_dest(1);
3111 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
3112 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
3116 $logger->info("circulator: hold is not for here..");
3117 $self->remote_hold($hold);
3122 sub checkin_handle_precat {
3124 my $copy = $self->copy;
3126 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
3127 $copy->status(OILS_COPY_STATUS_CATALOGING);
3128 $self->update_copy();
3129 $self->checkin_changed(1);
3130 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
3135 sub checkin_build_copy_transit {
3138 my $copy = $self->copy;
3139 my $transit = Fieldmapper::action::transit_copy->new;
3141 # if we are transiting an item to the shelf shelf, it's a hold transit
3142 if (my $hold = $self->remote_hold) {
3143 $transit = Fieldmapper::action::hold_transit_copy->new;
3144 $transit->hold($hold->id);
3146 # the item is going into transit, remove any shelf-iness
3147 if ($hold->current_shelf_lib or $hold->shelf_time) {
3148 $hold->clear_current_shelf_lib;
3149 $hold->clear_shelf_time;
3150 return $self->bail_on_events($self->editor->event)
3151 unless $self->editor->update_action_hold_request($hold);
3155 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
3156 $logger->info("circulator: transiting copy to $dest");
3158 $transit->source($self->circ_lib);
3159 $transit->dest($dest);
3160 $transit->target_copy($copy->id);
3161 $transit->source_send_time('now');
3162 $transit->copy_status( $U->copy_status($copy->status)->id );
3164 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
3166 if ($self->remote_hold) {
3167 return $self->bail_on_events($self->editor->event)
3168 unless $self->editor->create_action_hold_transit_copy($transit);
3170 return $self->bail_on_events($self->editor->event)
3171 unless $self->editor->create_action_transit_copy($transit);
3174 # ensure the transit is returned to the caller
3175 $self->transit($transit);
3177 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3179 $self->checkin_changed(1);
3183 sub hold_capture_is_possible {
3185 my $copy = $self->copy;
3187 # we've been explicitly told not to capture any holds
3188 return 0 if $self->capture eq 'nocapture';
3190 # See if this copy can fulfill any holds
3191 my $hold = $holdcode->find_nearest_permitted_hold(
3192 $self->editor, $copy, $self->editor->requestor, 1 # check_only
3194 return undef if ref $hold eq "HASH" and
3195 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
3199 sub reservation_capture_is_possible {
3201 my $copy = $self->copy;
3203 # we've been explicitly told not to capture any holds
3204 return 0 if $self->capture eq 'nocapture';
3206 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3207 my $resv = $booking_ses->request(
3208 "open-ils.booking.reservations.could_capture",
3209 $self->editor->authtoken, $copy->barcode
3211 $booking_ses->disconnect;
3212 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
3213 $self->push_events($resv);
3219 # returns true if the item was used (or may potentially be used
3220 # in subsequent calls) to capture a hold.
3221 sub attempt_checkin_hold_capture {
3223 my $copy = $self->copy;
3225 # we've been explicitly told not to capture any holds
3226 return 0 if $self->capture eq 'nocapture';
3228 # See if this copy can fulfill any holds
3229 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3230 $self->editor, $copy, $self->editor->requestor );
3233 $logger->debug("circulator: no potential permitted".
3234 "holds found for copy ".$copy->barcode);
3238 if($self->capture ne 'capture') {
3239 # see if this item is in a hold-capture-delay location
3240 my $location = $self->copy->location;
3241 if(!ref($location)) {
3242 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3243 $self->copy->location($location);
3245 if($U->is_true($location->hold_verify)) {
3246 $self->bail_on_events(
3247 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3252 $self->retarget($retarget);
3254 my $suppress_transit = 0;
3255 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3256 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3257 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3258 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3259 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3260 $suppress_transit = 1;
3261 $hold->pickup_lib($self->circ_lib);
3266 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3268 $hold->current_copy($copy->id);
3269 $hold->capture_time('now');
3270 $self->put_hold_on_shelf($hold)
3271 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3273 # prevent DB errors caused by fetching
3274 # holds from storage, and updating through cstore
3275 $hold->clear_fulfillment_time;
3276 $hold->clear_fulfillment_staff;
3277 $hold->clear_fulfillment_lib;
3278 $hold->clear_expire_time;
3279 $hold->clear_cancel_time;
3280 $hold->clear_prev_check_time unless $hold->prev_check_time;
3282 $self->bail_on_events($self->editor->event)
3283 unless $self->editor->update_action_hold_request($hold);
3285 $self->checkin_changed(1);
3287 return 0 if $self->bail_out;
3289 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3291 if ($hold->hold_type eq 'R') {
3292 $copy->status(OILS_COPY_STATUS_CATALOGING);
3293 $hold->fulfillment_time('now');
3294 $self->noop(1); # Block other transit/hold checks
3295 $self->bail_on_events($self->editor->event)
3296 unless $self->editor->update_action_hold_request($hold);
3298 # This hold was captured in the correct location
3299 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3300 $self->push_events(OpenILS::Event->new('SUCCESS'));
3302 #$self->do_hold_notify($hold->id);
3303 $self->notify_hold($hold->id);
3308 # Hold needs to be picked up elsewhere. Build a hold
3309 # transit and route the item.
3310 $self->checkin_build_hold_transit();
3311 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3312 return 0 if $self->bail_out;
3313 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3316 # make sure we save the copy status
3318 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3322 sub attempt_checkin_reservation_capture {
3324 my $copy = $self->copy;
3326 # we've been explicitly told not to capture any holds
3327 return 0 if $self->capture eq 'nocapture';
3329 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3330 my $evt = $booking_ses->request(
3331 "open-ils.booking.resources.capture_for_reservation",
3332 $self->editor->authtoken,
3334 1 # don't update copy - we probably have it locked
3336 $booking_ses->disconnect;
3338 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3340 "open-ils.booking.resources.capture_for_reservation " .
3341 "didn't return an event!"
3345 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3346 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3348 # not-transferable is an error event we'll pass on the user
3349 $logger->warn("reservation capture attempted against non-transferable item");
3350 $self->push_events($evt);
3352 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3353 # Re-retrieve copy as reservation capture may have changed
3354 # its status and whatnot.
3356 "circulator: booking capture win on copy " . $self->copy->id
3358 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3360 "circulator: changing copy " . $self->copy->id .
3361 "'s status from " . $self->copy->status . " to " .
3364 $self->copy->status($new_copy_status);
3367 $self->reservation($evt->{"payload"}->{"reservation"});
3369 if (exists $evt->{"payload"}->{"transit"}) {
3373 "org" => $evt->{"payload"}->{"transit"}->dest
3377 $self->checkin_changed(1);
3381 # other results are treated as "nothing to capture"
3385 sub do_hold_notify {
3386 my( $self, $holdid ) = @_;
3388 my $e = new_editor(xact => 1);
3389 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3391 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3392 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3394 $logger->info("circulator: running delayed hold notify process");
3396 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3397 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3399 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3400 hold_id => $holdid, requestor => $self->editor->requestor);
3402 $logger->debug("circulator: built hold notifier");
3404 if(!$notifier->event) {
3406 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3408 my $stat = $notifier->send_email_notify;
3409 if( $stat == '1' ) {
3410 $logger->info("circulator: hold notify succeeded for hold $holdid");
3414 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3417 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3421 sub retarget_holds {
3423 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3424 my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
3425 $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
3426 # no reason to wait for the return value
3430 sub checkin_build_hold_transit {
3433 my $copy = $self->copy;
3434 my $hold = $self->hold;
3435 my $trans = Fieldmapper::action::hold_transit_copy->new;
3437 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3439 $trans->hold($hold->id);
3440 $trans->source($self->circ_lib);
3441 $trans->dest($hold->pickup_lib);
3442 $trans->source_send_time("now");
3443 $trans->target_copy($copy->id);
3445 # when the copy gets to its destination, it will recover
3446 # this status - put it onto the holds shelf
3447 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3449 return $self->bail_on_events($self->editor->event)
3450 unless $self->editor->create_action_hold_transit_copy($trans);
3455 sub process_received_transit {
3457 my $copy = $self->copy;
3458 my $copyid = $self->copy->id;
3460 my $status_name = $U->copy_status($copy->status)->name;
3461 $logger->debug("circulator: attempting transit receive on ".
3462 "copy $copyid. Copy status is $status_name");
3464 my $transit = $self->transit;
3466 # Check if we are in a transit suppress range
3467 my $suppress_transit = 0;
3468 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3469 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3470 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3471 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3472 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3473 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3474 $suppress_transit = 1;
3475 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3479 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3480 # - this item is in-transit to a different location
3481 # - Or we are capturing holds as transits, so why create a new transit?
3483 my $tid = $transit->id;
3484 my $loc = $self->circ_lib;
3485 my $dest = $transit->dest;
3487 $logger->info("circulator: Fowarding transit on copy which is destined ".
3488 "for a different location. transit=$tid, copy=$copyid, current ".
3489 "location=$loc, destination location=$dest");
3491 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3493 # grab the associated hold object if available
3494 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3495 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3497 return $self->bail_on_events($evt);
3500 # The transit is received, set the receive time
3501 $transit->dest_recv_time('now');
3502 $self->bail_on_events($self->editor->event)
3503 unless $self->editor->update_action_transit_copy($transit);
3505 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3507 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3508 $copy->status( $transit->copy_status );
3509 $self->update_copy();
3510 return if $self->bail_out;
3514 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3517 # hold has arrived at destination, set shelf time
3518 $self->put_hold_on_shelf($hold);
3519 $self->bail_on_events($self->editor->event)
3520 unless $self->editor->update_action_hold_request($hold);
3521 return if $self->bail_out;
3523 $self->notify_hold($hold_transit->hold);
3526 $hold_transit = undef;
3527 $self->cancelled_hold_transit(1);
3528 $self->reshelve_copy(1);
3529 $self->fake_hold_dest(0);
3534 OpenILS::Event->new(
3537 payload => { transit => $transit, holdtransit => $hold_transit } ));
3539 return $hold_transit;
3543 # ------------------------------------------------------------------
3544 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3545 # ------------------------------------------------------------------
3546 sub put_hold_on_shelf {
3547 my($self, $hold) = @_;
3548 $hold->shelf_time('now');
3549 $hold->current_shelf_lib($self->circ_lib);
3550 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3556 my $reservation = shift;
3557 my $dt_parser = DateTime::Format::ISO8601->new;
3559 my $obj = $reservation ? $self->reservation : $self->circ;
3561 my $lost_bill_opts = $self->lost_bill_options;
3562 my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3563 # first, restore any voided overdues for lost, if needed
3564 if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3565 my $restore_od = $U->ou_ancestor_setting_value(
3566 $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3567 $self->editor) || 0;
3568 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3572 # next, handle normal overdue generation and apply stop_fines
3573 # XXX reservations don't have stop_fines
3574 # TODO revisit booking_reservation re: stop_fines support
3575 if ($reservation or !$obj->stop_fines) {
3578 # This is a crude check for whether we are in a grace period. The code
3579 # in generate_fines() does a more thorough job, so this exists solely
3580 # as a small optimization, and might be better off removed.
3582 # If we have a grace period
3583 if($obj->can('grace_period')) {
3584 # Parse out the due date
3585 my $due_date = $dt_parser->parse_datetime( clean_ISO8601($obj->due_date) );
3586 # Add the grace period to the due date
3587 $due_date->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($obj->grace_period));
3588 # Don't generate fines on circs still in grace period
3589 $skip_for_grace = $due_date > DateTime->now;
3591 $CC->generate_fines({circs => [$obj], editor => $self->editor})
3592 unless $skip_for_grace;
3594 if (!$reservation and !$obj->stop_fines) {
3595 $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3596 $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3597 $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3598 $obj->stop_fines_time('now');
3599 $obj->stop_fines_time($self->backdate) if $self->backdate;
3600 $self->editor->update_action_circulation($obj);
3604 # finally, handle voiding of lost item and processing fees
3605 if ($self->needs_lost_bill_handling) {
3606 my $void_cost = $U->ou_ancestor_setting_value(
3607 $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3608 $self->editor) || 0;
3609 my $void_proc_fee = $U->ou_ancestor_setting_value(
3610 $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3611 $self->editor) || 0;
3612 $self->checkin_handle_lost_or_lo_now_found(
3613 $lost_bill_opts->{void_cost_btype},
3614 $lost_bill_opts->{is_longoverdue}) if $void_cost;
3615 $self->checkin_handle_lost_or_lo_now_found(
3616 $lost_bill_opts->{void_fee_btype},
3617 $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3623 sub checkin_handle_circ_start {
3625 my $circ = $self->circ;
3626 my $copy = $self->copy;
3630 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3632 # backdate the circ if necessary
3633 if($self->backdate) {
3634 my $evt = $self->checkin_handle_backdate;
3635 return $self->bail_on_events($evt) if $evt;
3638 # Set the checkin vars since we have the item
3639 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3641 # capture the true scan time for back-dated checkins
3642 $circ->checkin_scan_time('now');
3644 $circ->checkin_staff($self->editor->requestor->id);
3645 $circ->checkin_lib($self->circ_lib);
3646 $circ->checkin_workstation($self->editor->requestor->wsid);
3648 my $circ_lib = (ref $self->copy->circ_lib) ?
3649 $self->copy->circ_lib->id : $self->copy->circ_lib;
3650 my $stat = $U->copy_status($self->copy->status)->id;
3652 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3653 # we will now handle lost fines, but the copy will retain its 'lost'
3654 # status if it needs to transit home unless lost_immediately_available
3657 # if we decide to also delay fine handling until the item arrives home,
3658 # we will need to call lost fine handling code both when checking items
3659 # in and also when receiving transits
3660 $self->checkin_handle_lost($circ_lib);
3661 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3662 # same process as above.
3663 $self->checkin_handle_long_overdue($circ_lib);
3664 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3665 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3667 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3668 $self->copy->status($U->copy_status($next_status));
3675 sub checkin_handle_circ_finish {
3677 my $e = $self->editor;
3678 my $circ = $self->circ;
3680 # Do one last check before the final circulation update to see
3681 # if the xact_finish value should be set or not.
3683 # The underlying money.billable_xact may have been updated to
3684 # reflect a change in xact_finish during checkin bills handling,
3685 # however we can't simply refresh the circulation from the DB,
3686 # because other changes may be pending. Instead, reproduce the
3687 # xact_finish check here. It won't hurt to do it again.
3689 my $sum = $e->retrieve_money_billable_transaction_summary($circ->id);
3690 if ($sum) { # is this test still needed?
3692 my $balance = $sum->balance_owed;
3694 if ($balance == 0) {
3695 $circ->xact_finish('now');
3697 $circ->clear_xact_finish;
3700 $logger->info("circulator: $balance is owed on this circulation");
3703 return $self->bail_on_events($e->event)
3704 unless $e->update_action_circulation($circ);
3709 # ------------------------------------------------------------------
3710 # See if we need to void billings, etc. for lost checkin
3711 # ------------------------------------------------------------------
3712 sub checkin_handle_lost {
3714 my $circ_lib = shift;
3716 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3717 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3719 $self->lost_bill_options({
3720 circ_lib => $circ_lib,
3721 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3722 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3723 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3724 void_cost_btype => 3,
3728 return $self->checkin_handle_lost_or_longoverdue(
3729 circ_lib => $circ_lib,
3730 max_return => $max_return,
3731 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3732 ous_use_last_activity => undef # not supported for LOST checkin
3736 # ------------------------------------------------------------------
3737 # See if we need to void billings, etc. for long-overdue checkin
3738 # note: not using constants below since they serve little purpose
3739 # for single-use strings that are descriptive in their own right
3740 # and mostly just complicate debugging.
3741 # ------------------------------------------------------------------
3742 sub checkin_handle_long_overdue {
3744 my $circ_lib = shift;
3746 $logger->info("circulator: processing long-overdue checkin...");
3748 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3749 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3751 $self->lost_bill_options({
3752 circ_lib => $circ_lib,
3753 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3754 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3755 is_longoverdue => 1,
3756 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3757 void_cost_btype => 10,
3758 void_fee_btype => 11
3761 return $self->checkin_handle_lost_or_longoverdue(
3762 circ_lib => $circ_lib,
3763 max_return => $max_return,
3764 ous_immediately_available => 'circ.longoverdue_immediately_available',
3765 ous_use_last_activity =>
3766 'circ.longoverdue.use_last_activity_date_on_return'
3770 # last billing activity is last payment time, last billing time, or the
3771 # circ due date. If the relevant "use last activity" org unit setting is
3772 # false/unset, then last billing activity is always the due date.
3773 sub get_circ_last_billing_activity {
3775 my $circ_lib = shift;
3776 my $setting = shift;
3777 my $date = $self->circ->due_date;
3779 return $date unless $setting and
3780 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3782 my $xact = $self->editor->retrieve_money_billable_transaction([
3784 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3787 if ($xact->summary) {
3788 $date = $xact->summary->last_payment_ts ||
3789 $xact->summary->last_billing_ts ||
3790 $self->circ->due_date;
3797 sub checkin_handle_lost_or_longoverdue {
3798 my ($self, %args) = @_;
3800 my $circ = $self->circ;
3801 my $max_return = $args{max_return};
3802 my $circ_lib = $args{circ_lib};
3807 $self->get_circ_last_billing_activity(
3808 $circ_lib, $args{ous_use_last_activity});
3811 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3812 $tm[5] -= 1 if $tm[5] > 0;
3813 my $due = timelocal(int($tm[1]), int($tm[2]),
3814 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3817 OpenILS::Utils::DateTime->interval_to_seconds($max_return) + int($due);
3819 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3820 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3821 "DUE: $due LAST: $last_chance");
3823 $max_return = 0 if $today < $last_chance;
3829 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3830 "return interval. skipping fine/fee voiding, etc.");
3832 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3834 $logger->info("circulator: check-in of lost/lo item having a balance ".
3835 "of zero, skipping fine/fee voiding and reinstatement.");
3837 } else { # within max-return interval or no interval defined
3839 $logger->info("circulator: check-in of lost/lo item is within the ".
3840 "max return interval (or no interval is defined). Proceeding ".
3841 "with fine/fee voiding, etc.");
3843 $self->needs_lost_bill_handling(1);
3846 if ($circ_lib != $self->circ_lib) {
3847 # if the item is not home, check to see if we want to retain the
3848 # lost/longoverdue status at this point in the process
3850 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3851 $args{ous_immediately_available}, $self->editor) || 0;
3853 if ($immediately_available) {
3854 # item status does not need to be retained, so give it a
3855 # reshelving status as if it were a normal checkin
3856 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3857 $self->copy->status($U->copy_status($next_status));
3860 $logger->info("circulator: leaving lost/longoverdue copy".
3861 " status in place on checkin");
3864 # lost/longoverdue item is home and processed, treat like a normal
3865 # checkin from this point on
3866 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3867 $self->copy->status($U->copy_status($next_status));
3873 sub checkin_handle_backdate {
3876 # ------------------------------------------------------------------
3877 # clean up the backdate for date comparison
3878 # XXX We are currently taking the due-time from the original due-date,
3879 # not the input. Do we need to do this? This certainly interferes with
3880 # backdating of hourly checkouts, but that is likely a very rare case.
3881 # ------------------------------------------------------------------
3882 my $bd = clean_ISO8601($self->backdate);
3883 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($self->circ->due_date));
3884 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3885 $new_date->set_hour($original_date->hour());
3886 $new_date->set_minute($original_date->minute());
3887 if ($new_date >= DateTime->now) {
3888 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3889 # $self->backdate() autoload handler ignores undef values.
3890 # Clear the backdate manually.
3891 $logger->info("circulator: ignoring future backdate: $new_date");
3892 delete $self->{backdate};
3894 $self->backdate(clean_ISO8601($new_date->datetime()));
3901 sub check_checkin_copy_status {
3903 my $copy = $self->copy;
3905 my $status = $U->copy_status($copy->status)->id;
3908 if( $self->new_copy_alerts ||
3909 $status == OILS_COPY_STATUS_AVAILABLE ||
3910 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3911 $status == OILS_COPY_STATUS_IN_PROCESS ||
3912 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3913 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3914 $status == OILS_COPY_STATUS_CATALOGING ||
3915 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3916 $status == OILS_COPY_STATUS_CANCELED_TRANSIT ||
3917 $status == OILS_COPY_STATUS_RESHELVING );
3919 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3920 if( $status == OILS_COPY_STATUS_LOST );
3922 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3923 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3925 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3926 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3928 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3929 if( $status == OILS_COPY_STATUS_MISSING );
3931 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3936 # --------------------------------------------------------------------------
3937 # On checkin, we need to return as many relevant objects as we can
3938 # --------------------------------------------------------------------------
3939 sub checkin_flesh_events {
3942 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3943 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3944 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3947 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3950 if($self->hold and !$self->hold->cancel_time) {
3951 $hold = $self->hold;
3952 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3956 # update our copy of the circ object and
3957 # flesh the billing summary data
3959 $self->editor->retrieve_action_circulation([
3963 circ => ['billable_transaction'],
3972 # flesh some patron fields before returning
3974 $self->editor->retrieve_actor_user([
3979 au => ['card', 'billing_address', 'mailing_address']
3986 if ($self->latest_inventory) {
3987 # flesh some workstation fields before returning
3988 $self->latest_inventory->inventory_workstation(
3989 $self->editor->retrieve_actor_workstation([$self->latest_inventory->inventory_workstation])
3993 if($self->latest_inventory && !$self->latest_inventory->id) {
3994 my $alci = $self->editor->search_asset_latest_inventory(
3995 {copy => $self->latest_inventory->copy}
3998 $self->latest_inventory->id($alci->[0]->id);
4001 $self->copy->latest_inventory($self->latest_inventory);
4003 for my $evt (@{$self->events}) {
4006 $payload->{copy} = $U->unflesh_copy($self->copy);
4007 $payload->{volume} = $self->volume;
4008 $payload->{record} = $record,
4009 $payload->{circ} = $self->circ;
4010 $payload->{transit} = $self->transit;
4011 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
4012 $payload->{hold} = $hold;
4013 $payload->{patron} = $self->patron;
4014 $payload->{reservation} = $self->reservation
4015 unless (not $self->reservation or $self->reservation->cancel_time);
4016 $payload->{latest_inventory} = $self->latest_inventory;
4017 if ($self->do_inventory_update) { $payload->{do_inventory_update} = 1; }
4019 $evt->{payload} = $payload;
4024 my( $self, $msg ) = @_;
4025 my $bc = ($self->copy) ? $self->copy->barcode :
4028 my $usr = ($self->patron) ? $self->patron->id : "";
4029 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
4030 ", recipient=$usr, copy=$bc");
4037 $self->log_me("do_renew()");
4039 # Make sure there is an open circ to renew
4040 my $usrid = $self->patron->id if $self->patron;
4041 my $circ = $self->editor->search_action_circulation({
4042 target_copy => $self->copy->id,
4043 xact_finish => undef,
4044 checkin_time => undef,
4045 ($usrid ? (usr => $usrid) : ())
4048 return $self->bail_on_events($self->editor->event) unless $circ;
4050 # A user is not allowed to renew another user's items without permission
4051 unless( $circ->usr eq $self->editor->requestor->id ) {
4052 return $self->bail_on_events($self->editor->events)
4053 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
4056 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
4057 if $circ->renewal_remaining < 1;
4059 $self->push_events(OpenILS::Event->new('MAX_AUTO_RENEWALS_REACHED'))
4060 if $api =~ /renew.auto/ and $circ->auto_renewal_remaining < 1;
4061 # -----------------------------------------------------------------
4063 $self->parent_circ($circ->id);
4064 $self->renewal_remaining( $circ->renewal_remaining - 1 );
4065 $self->auto_renewal_remaining( $circ->auto_renewal_remaining - 1 ) if (defined($circ->auto_renewal_remaining));
4068 # Opac renewal - re-use circ library from original circ (unless told not to)
4069 if($self->opac_renewal or $api =~ /renew.auto/) {
4070 unless(defined($opac_renewal_use_circ_lib)) {
4071 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
4072 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
4073 $opac_renewal_use_circ_lib = 1;
4076 $opac_renewal_use_circ_lib = 0;
4079 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
4082 # Desk renewal - re-use circ library from original circ (unless told not to)
4083 if($self->desk_renewal) {
4084 unless(defined($desk_renewal_use_circ_lib)) {
4085 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
4086 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
4087 $desk_renewal_use_circ_lib = 1;
4090 $desk_renewal_use_circ_lib = 0;
4093 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
4096 # Run the fine generator against the old circ
4097 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
4098 # a few lines down. Commenting out, for now.
4099 #$self->handle_fines;
4101 $self->run_renew_permit;
4104 $self->do_checkin();
4105 return if $self->bail_out;
4107 unless( $self->permit_override ) {
4109 return if $self->bail_out;
4110 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
4111 $self->remove_event('ITEM_NOT_CATALOGED');
4114 $self->override_events;
4115 return if $self->bail_out;
4118 $self->do_checkout();
4123 my( $self, $evt ) = @_;
4124 $evt = (ref $evt) ? $evt->{textcode} : $evt;
4125 $logger->debug("circulator: removing event from list: $evt");
4126 my @events = @{$self->events};
4127 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
4132 my( $self, $evt ) = @_;
4133 $evt = (ref $evt) ? $evt->{textcode} : $evt;
4134 return grep { $_->{textcode} eq $evt } @{$self->events};
4138 sub run_renew_permit {
4141 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
4142 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
4143 $self->editor, $self->copy, $self->editor->requestor, 1
4145 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
4148 my $results = $self->run_indb_circ_test;
4149 $self->push_events($self->matrix_test_result_events)
4150 unless $self->circ_test_success;
4154 # XXX: The primary mechanism for storing circ history is now handled
4155 # by tracking real circulation objects instead of bibs in a bucket.
4156 # However, this code is disabled by default and could be useful
4157 # some day, so may as well leave it for now.
4158 sub append_reading_list {
4162 $self->is_checkout and
4168 # verify history is globally enabled and uses the bucket mechanism
4169 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
4170 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
4172 return undef unless $htype and $htype eq 'bucket';
4174 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
4176 # verify the patron wants to retain the hisory
4177 my $setting = $e->search_actor_user_setting(
4178 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
4180 unless($setting and $setting->value) {
4185 my $bkt = $e->search_container_copy_bucket(
4186 {owner => $self->patron->id, btype => 'circ_history'})->[0];
4191 # find the next item position
4192 my $last_item = $e->search_container_copy_bucket_item(
4193 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
4194 $pos = $last_item->pos + 1 if $last_item;
4197 # create the history bucket if necessary
4198 $bkt = Fieldmapper::container::copy_bucket->new;
4199 $bkt->owner($self->patron->id);
4201 $bkt->btype('circ_history');
4203 $e->create_container_copy_bucket($bkt) or return $e->die_event;
4206 my $item = Fieldmapper::container::copy_bucket_item->new;
4208 $item->bucket($bkt->id);
4209 $item->target_copy($self->copy->id);
4212 $e->create_container_copy_bucket_item($item) or return $e->die_event;
4219 sub make_trigger_events {
4221 return unless $self->circ;
4222 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
4223 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
4224 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
4229 sub checkin_handle_lost_or_lo_now_found {
4230 my ($self, $bill_type, $is_longoverdue) = @_;
4232 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4234 $logger->debug("voiding $tag item billings");
4235 my $result = $CC->void_or_zero_bills_of_type($self->editor, $self->circ, $self->copy, $bill_type, "$tag ITEM RETURNED");
4236 $self->bail_on_events($self->editor->event) if ($result);
4239 sub checkin_handle_lost_or_lo_now_found_restore_od {
4241 my $circ_lib = shift;
4242 my $is_longoverdue = shift;
4243 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4245 # ------------------------------------------------------------------
4246 # restore those overdue charges voided when item was set to lost
4247 # ------------------------------------------------------------------
4249 my $ods = $self->editor->search_money_billing([
4251 xact => $self->circ->id,
4255 order_by => {mb => 'billing_ts desc'}
4259 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4260 # Because actual users get up to all kinds of unexpectedness, we
4261 # only recreate up to $circ->max_fine in bills. I know you think
4262 # it wouldn't happen that bills could get created, voided, and
4263 # recreated more than once, but I guaran-damn-tee you that it will
4265 if ($ods && @$ods) {
4266 my $void_amount = 0;
4267 my $void_max = $self->circ->max_fine();
4268 # search for overdues voided the new way (aka "adjusted")
4269 my @billings = map {$_->id()} @$ods;
4270 my $voids = $self->editor->search_money_account_adjustment(
4272 billing => \@billings
4276 map {$void_amount += $_->amount()} @$voids;
4278 # if no adjustments found, assume they were voided the old way (aka "voided")
4279 for my $bill (@$ods) {
4280 if( $U->is_true($bill->voided) ) {
4281 $void_amount += $bill->amount();
4287 ($void_amount < $void_max ? $void_amount : $void_max),
4289 $ods->[0]->billing_type(),
4291 "System: $tag RETURNED - OVERDUES REINSTATED",
4292 $ods->[-1]->period_start(),
4293 $ods->[0]->period_end() # date this restoration the same as the last overdue (for possible subsequent fine generation)