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;
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 OpenSRF::Utils 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/
427 overrides_per_copy_alerts
469 recurring_fines_level
474 auto_renewal_remaining
483 cancelled_hold_transit
490 circ_matrix_matchpoint
501 claims_never_checked_out
514 dont_change_lost_zero
516 needs_lost_bill_handling
522 my $type = ref($self) or die "$self is not an object";
524 my $name = $AUTOLOAD;
527 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
528 $logger->error("circulator: $type: invalid autoload field: $name");
529 die "$type: invalid autoload field: $name\n"
534 *{"${type}::${name}"} = sub {
537 $s->{$name} = $v if defined $v;
541 return $self->$name($data);
546 my( $class, $auth, %args ) = @_;
547 $class = ref($class) || $class;
548 my $self = bless( {}, $class );
551 $self->editor(new_editor(xact => 1, authtoken => $auth));
553 unless( $self->editor->checkauth ) {
554 $self->bail_on_events($self->editor->event);
558 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
560 $self->$_($args{$_}) for keys %args;
563 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
565 # if this is a renewal, default to desk_renewal
566 $self->desk_renewal(1) unless
567 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
569 $self->capture('') unless $self->capture;
571 unless(%user_groups) {
572 my $gps = $self->editor->retrieve_all_permission_grp_tree;
573 %user_groups = map { $_->id => $_ } @$gps;
580 # --------------------------------------------------------------------------
581 # True if we should discontinue processing
582 # --------------------------------------------------------------------------
584 my( $self, $bool ) = @_;
585 if( defined $bool ) {
586 $logger->info("circulator: BAILING OUT") if $bool;
587 $self->{bail_out} = $bool;
589 return $self->{bail_out};
594 my( $self, @evts ) = @_;
597 $e->{payload} = $self->copy if
598 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
600 $logger->info("circulator: pushing event ".$e->{textcode});
601 push( @{$self->events}, $e ) unless
602 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
608 return '' if $self->skip_permit_key;
609 my $key = md5_hex( time() . rand() . "$$" );
610 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
611 return $self->permit_key($key);
614 sub check_permit_key {
616 return 1 if $self->skip_permit_key;
617 my $key = $self->permit_key;
618 return 0 unless $key;
619 my $k = "oils_permit_key_$key";
620 my $one = $self->cache_handle->get_cache($k);
621 $self->cache_handle->delete_cache($k);
622 return ($one) ? 1 : 0;
625 sub seems_like_reservation {
628 # Some words about the following method:
629 # 1) It requires the VIEW_USER permission, but that's not an
630 # issue, right, since all staff should have that?
631 # 2) It returns only one reservation at a time, even if an item can be
632 # and is currently overbooked. Hmmm....
633 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
634 my $result = $booking_ses->request(
635 "open-ils.booking.reservations.by_returnable_resource_barcode",
636 $self->editor->authtoken,
639 $booking_ses->disconnect;
641 return $self->bail_on_events($result) if defined $U->event_code($result);
644 $self->reservation(shift @$result);
652 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
653 sub save_trimmed_copy {
654 my ($self, $copy) = @_;
657 $self->volume($copy->call_number);
658 $self->title($self->volume->record);
659 $self->copy->call_number($self->volume->id);
660 $self->volume->record($self->title->id);
661 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
662 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
663 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
664 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
668 sub collect_user_copy_alerts {
670 my $e = $self->editor;
673 my $alerts = $e->search_asset_copy_alert([
674 {copy => $self->copy->id, ack_time => undef},
675 {flesh => 1, flesh_fields => { aca => [ qw/ alert_type / ] }}
677 if (ref $alerts eq "ARRAY") {
678 $logger->info("circulator: found " . scalar(@$alerts) . " alerts for copy " .
680 $self->user_copy_alerts($alerts);
685 sub filter_user_copy_alerts {
688 my $e = $self->editor;
690 if(my $alerts = $self->user_copy_alerts) {
692 my $suppress_orgs = $U->get_org_full_path($self->circ_lib);
693 my $suppressions = $e->search_actor_copy_alert_suppress(
694 {org => $suppress_orgs}
698 foreach my $a (@$alerts) {
699 # filter on event type
700 if (defined $a->alert_type) {
701 next if ($a->alert_type->event eq 'CHECKIN' && !$self->is_checkin && !$self->is_renewal);
702 next if ($a->alert_type->event eq 'CHECKOUT' && !$self->is_checkout && !$self->is_renewal);
703 next if (defined $a->alert_type->in_renew && $U->is_true($a->alert_type->in_renew) && !$self->is_renewal);
704 next if (defined $a->alert_type->in_renew && !$U->is_true($a->alert_type->in_renew) && $self->is_renewal);
707 # filter on suppression
708 next if (grep { $a->alert_type->id == $_->alert_type} @$suppressions);
710 # filter on "only at circ lib"
711 if (defined $a->alert_type->at_circ) {
712 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
713 $self->copy->circ_lib->id : $self->copy->circ_lib;
714 my $orgs = $U->get_org_descendants($copy_circ_lib);
716 if ($U->is_true($a->alert_type->invert_location)) {
717 next if (grep {$_ == $self->circ_lib} @$orgs);
719 next unless (grep {$_ == $self->circ_lib} @$orgs);
723 # filter on "only at owning lib"
724 if (defined $a->alert_type->at_owning) {
725 my $copy_owning_lib = (ref $self->volume->owning_lib) ?
726 $self->volume->owning_lib->id : $self->volume->owning_lib;
727 my $orgs = $U->get_org_descendants($copy_owning_lib);
729 if ($U->is_true($a->alert_type->invert_location)) {
730 next if (grep {$_ == $self->circ_lib} @$orgs);
732 next unless (grep {$_ == $self->circ_lib} @$orgs);
736 $a->alert_type->next_status([$U->unique_unnested_numbers($a->alert_type->next_status)]);
738 push @final_alerts, $a;
741 $self->user_copy_alerts(\@final_alerts);
745 sub generate_system_copy_alerts {
747 return unless($self->copy);
749 # don't create system copy alerts if the copy
750 # is in a normal state; we're assuming that there's
751 # never a need to generate a popup for each and every
752 # checkin or checkout of normal items. If this assumption
753 # proves false, then we'll need to add a way to explicitly specify
754 # that a copy alert type should never generate a system copy alert
755 return if $self->copy_state eq 'NORMAL';
757 my $e = $self->editor;
759 my $suppress_orgs = $U->get_org_full_path($self->circ_lib);
760 my $suppressions = $e->search_actor_copy_alert_suppress(
761 {org => $suppress_orgs}
764 # events we care about ...
766 push(@$event, 'CHECKIN') if $self->is_checkin;
767 push(@$event, 'CHECKOUT') if $self->is_checkout;
768 return unless scalar(@$event);
770 my $alert_orgs = $U->get_org_ancestors($self->circ_lib);
771 my $alert_types = $e->search_config_copy_alert_type({
773 scope_org => $alert_orgs,
775 state => $self->copy_state,
776 '-or' => [ { in_renew => $self->is_renewal }, { in_renew => undef } ],
780 foreach my $a (@$alert_types) {
781 # filter on "only at circ lib"
782 if (defined $a->at_circ) {
783 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
784 $self->copy->circ_lib->id : $self->copy->circ_lib;
785 my $orgs = $U->get_org_descendants($copy_circ_lib);
787 if ($U->is_true($a->invert_location)) {
788 next if (grep {$_ == $self->circ_lib} @$orgs);
790 next unless (grep {$_ == $self->circ_lib} @$orgs);
794 # filter on "only at owning lib"
795 if (defined $a->at_owning) {
796 my $copy_owning_lib = (ref $self->volume->owning_lib) ?
797 $self->volume->owning_lib->id : $self->volume->owning_lib;
798 my $orgs = $U->get_org_descendants($copy_owning_lib);
800 if ($U->is_true($a->invert_location)) {
801 next if (grep {$_ == $self->circ_lib} @$orgs);
803 next unless (grep {$_ == $self->circ_lib} @$orgs);
807 push @final_types, $a;
811 $logger->info("circulator: found " . scalar(@final_types) . " system alert types for copy" .
817 # keep track of conditions corresponding to suppressed
818 # system alerts, as these may be used to overridee
819 # certain old-style-events
820 my %auto_override_conditions = ();
821 foreach my $t (@final_types) {
822 if ($t->next_status) {
823 if (grep { $t->id == $_->alert_type } @$suppressions) {
826 $t->next_status([$U->unique_unnested_numbers($t->next_status)]);
830 my $alert = new Fieldmapper::asset::copy_alert ();
831 $alert->alert_type($t->id);
832 $alert->copy($self->copy->id);
834 $alert->create_staff($e->requestor->id);
835 $alert->create_time('now');
836 $alert->ack_staff($e->requestor->id);
837 $alert->ack_time('now');
839 $alert = $e->create_asset_copy_alert($alert);
843 $alert->alert_type($t->clone);
845 push(@{$self->next_copy_status}, @{$t->next_status}) if ($t->next_status);
846 if (grep {$_->alert_type == $t->id} @$suppressions) {
847 $auto_override_conditions{join("\t", $t->state, $t->event)} = 1;
849 push(@alerts, $alert) unless (grep {$_->alert_type == $t->id} @$suppressions);
852 $self->system_copy_alerts(\@alerts);
853 $self->overrides_per_copy_alerts(\%auto_override_conditions);
856 sub add_overrides_from_system_copy_alerts {
858 my $e = $self->editor;
860 foreach my $condition (keys %{$self->overrides_per_copy_alerts()}) {
861 if (exists $COPY_ALERT_OVERRIDES{$condition}) {
863 push @{$self->override_args->{events}}, @{ $COPY_ALERT_OVERRIDES{$condition} };
864 # special handling for long-overdue and lost checkouts
865 if (grep { $_ eq 'OPEN_CIRCULATION_EXISTS' } @{ $COPY_ALERT_OVERRIDES{$condition} }) {
866 my $state = (split /\t/, $condition, -1)[0];
868 if ($state eq 'LOST' or $state eq 'LOST_AND_PAID') {
869 $setting = 'circ.copy_alerts.forgive_fines_on_lost_checkin';
870 } elsif ($state eq 'LONGOVERDUE') {
871 $setting = 'circ.copy_alerts.forgive_fines_on_long_overdue_checkin';
875 my $forgive = $U->ou_ancestor_setting_value(
876 $self->circ_lib, $setting, $e
878 if ($U->is_true($forgive)) {
879 $self->void_overdues(1);
881 $self->noop(1); # do not attempt transits, just check it in
890 my $e = $self->editor;
892 $self->next_copy_status([]) unless (defined $self->next_copy_status);
893 $self->overrides_per_copy_alerts({}) unless (defined $self->overrides_per_copy_alerts);
895 # --------------------------------------------------------------------------
896 # Grab the fleshed copy
897 # --------------------------------------------------------------------------
898 unless($self->is_noncat) {
901 $copy = $e->retrieve_asset_copy(
902 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
904 } elsif( $self->copy_barcode ) {
906 $copy = $e->search_asset_copy(
907 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
908 } elsif( $self->reservation ) {
909 my $res = $e->json_query(
911 "select" => {"acp" => ["id"]},
916 "field" => "barcode",
920 "field" => "current_resource"
929 "id" => (ref $self->reservation) ?
930 $self->reservation->id : $self->reservation
935 if (ref $res eq "ARRAY" and scalar @$res) {
936 $logger->info("circulator: mapped reservation " .
937 $self->reservation . " to copy " . $res->[0]->{"id"});
938 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
943 $self->save_trimmed_copy($copy);
948 {from => ['asset.copy_state', $copy->id]}
949 )->[0]{'asset.copy_state'}
952 $self->generate_system_copy_alerts;
953 $self->add_overrides_from_system_copy_alerts;
954 $self->collect_user_copy_alerts;
955 $self->filter_user_copy_alerts;
958 # We can't renew if there is no copy
959 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
960 if $self->is_renewal;
965 # --------------------------------------------------------------------------
967 # --------------------------------------------------------------------------
971 flesh_fields => {au => [ qw/ card / ]}
974 if( $self->patron_id ) {
975 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
976 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
978 } elsif( $self->patron_barcode ) {
980 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
981 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
982 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
984 $patron = $e->retrieve_actor_user($card->usr)
985 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
987 # Use the card we looked up, not the patron's primary, for card active checks
988 $patron->card($card);
991 if( my $copy = $self->copy ) {
994 $flesh->{flesh_fields}->{circ} = ['usr'];
996 my $circ = $e->search_action_circulation([
997 {target_copy => $copy->id, checkin_time => undef}, $flesh
1001 $patron = $circ->usr;
1002 $circ->usr($patron->id); # de-flesh for consistency
1008 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
1009 unless $self->patron($patron) or $self->is_checkin;
1011 unless($self->is_checkin) {
1013 # Check for inactivity and patron reg. expiration
1015 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
1016 unless $U->is_true($patron->active);
1018 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
1019 unless $U->is_true($patron->card->active);
1021 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
1022 cleanse_ISO8601($patron->expire_date));
1024 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
1025 if( CORE::time > $expire->epoch ) ;
1030 # --------------------------------------------------------------------------
1031 # Does the circ permit work
1032 # --------------------------------------------------------------------------
1036 $self->log_me("do_permit()");
1038 unless( $self->editor->requestor->id == $self->patron->id ) {
1039 return $self->bail_on_events($self->editor->event)
1040 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
1043 $self->check_captured_holds();
1044 $self->do_copy_checks();
1045 return if $self->bail_out;
1046 $self->run_patron_permit_scripts();
1047 $self->run_copy_permit_scripts()
1048 unless $self->is_precat or $self->is_noncat;
1049 $self->check_item_deposit_events();
1050 $self->override_events();
1051 return if $self->bail_out;
1053 if($self->is_precat and not $self->request_precat) {
1055 OpenILS::Event->new(
1056 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
1057 return $self->bail_out(1) unless $self->is_renewal;
1061 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
1064 sub check_item_deposit_events {
1066 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
1067 if $self->is_deposit and not $self->is_deposit_exempt;
1068 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
1069 if $self->is_rental and not $self->is_rental_exempt;
1072 # returns true if the user is not required to pay deposits
1073 sub is_deposit_exempt {
1075 my $pid = (ref $self->patron->profile) ?
1076 $self->patron->profile->id : $self->patron->profile;
1077 my $groups = $U->ou_ancestor_setting_value(
1078 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
1079 for my $grp (@$groups) {
1080 return 1 if $self->is_group_descendant($grp, $pid);
1085 # returns true if the user is not required to pay rental fees
1086 sub is_rental_exempt {
1088 my $pid = (ref $self->patron->profile) ?
1089 $self->patron->profile->id : $self->patron->profile;
1090 my $groups = $U->ou_ancestor_setting_value(
1091 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
1092 for my $grp (@$groups) {
1093 return 1 if $self->is_group_descendant($grp, $pid);
1098 sub is_group_descendant {
1099 my($self, $p_id, $c_id) = @_;
1100 return 0 unless defined $p_id and defined $c_id;
1101 return 1 if $c_id == $p_id;
1102 while(my $grp = $user_groups{$c_id}) {
1103 $c_id = $grp->parent;
1104 return 0 unless defined $c_id;
1105 return 1 if $c_id == $p_id;
1110 sub check_captured_holds {
1112 my $copy = $self->copy;
1113 my $patron = $self->patron;
1115 return undef unless $copy;
1117 my $s = $U->copy_status($copy->status)->id;
1118 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1119 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
1121 # Item is on the holds shelf, make sure it's going to the right person
1122 my $hold = $self->editor->search_action_hold_request(
1125 current_copy => $copy->id ,
1126 capture_time => { '!=' => undef },
1127 cancel_time => undef,
1128 fulfillment_time => undef
1132 flesh_fields => { ahr => ['usr'] }
1137 if ($hold and $hold->usr->id == $patron->id) {
1138 $self->checkout_is_for_hold(1);
1142 my $holdau = $hold->usr;
1145 $payload->{patron_name} = $holdau->first_given_name . ' ' . $holdau->family_name;
1146 $payload->{patron_id} = $holdau->id;
1148 $payload->{patron_name} = "???";
1150 $payload->{hold_id} = $hold->id;
1151 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF',
1152 payload => $payload));
1155 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1160 sub do_copy_checks {
1162 my $copy = $self->copy;
1163 return unless $copy;
1165 my $stat = $U->copy_status($copy->status)->id;
1167 # We cannot check out a copy if it is in-transit
1168 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1169 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1172 $self->handle_claims_returned();
1173 return if $self->bail_out;
1175 # no claims returned circ was found, check if there is any open circ
1176 unless( $self->is_renewal ) {
1178 my $circs = $self->editor->search_action_circulation(
1179 { target_copy => $copy->id, checkin_time => undef }
1182 if(my $old_circ = $circs->[0]) { # an open circ was found
1184 my $payload = {copy => $copy};
1186 if($old_circ->usr == $self->patron->id) {
1188 $payload->{old_circ} = $old_circ;
1190 # If there is an open circulation on the checkout item and an auto-renew
1191 # interval is defined, inform the caller that they should go
1192 # ahead and renew the item instead of warning about open circulations.
1194 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1196 'circ.checkout_auto_renew_age',
1200 if($auto_renew_intvl) {
1201 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1202 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1204 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1205 $payload->{auto_renew} = 1;
1210 return $self->bail_on_events(
1211 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1217 my $LEGACY_CIRC_EVENT_MAP = {
1218 'no_item' => 'ITEM_NOT_CATALOGED',
1219 'actor.usr.barred' => 'PATRON_BARRED',
1220 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1221 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1222 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1223 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1224 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1225 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1226 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1227 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1228 'config.circ_matrix_test.total_copy_hold_ratio' =>
1229 'TOTAL_HOLD_COPY_RATIO_EXCEEDED',
1230 'config.circ_matrix_test.available_copy_hold_ratio' =>
1231 'AVAIL_HOLD_COPY_RATIO_EXCEEDED'
1235 # ---------------------------------------------------------------------
1236 # This pushes any patron-related events into the list but does not
1237 # set bail_out for any events
1238 # ---------------------------------------------------------------------
1239 sub run_patron_permit_scripts {
1241 my $patronid = $self->patron->id;
1246 my $results = $self->run_indb_circ_test;
1247 unless($self->circ_test_success) {
1248 my @trimmed_results;
1250 if ($self->is_noncat) {
1251 # no_item result is OK during noncat checkout
1252 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1256 if ($self->checkout_is_for_hold) {
1257 # if this checkout will fulfill a hold, ignore CIRC blocks
1258 # and rely instead on the (later-checked) FULFILL block
1260 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1261 my $fblock_pens = $self->editor->search_config_standing_penalty(
1262 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1264 for my $res (@$results) {
1265 my $name = $res->{fail_part} || '';
1266 next if grep {$_->name eq $name} @$fblock_pens;
1267 push(@trimmed_results, $res);
1271 # not for hold or noncat
1272 @trimmed_results = @$results;
1276 # update the final set of test results
1277 $self->matrix_test_result(\@trimmed_results);
1279 push @allevents, $self->matrix_test_result_events;
1283 $_->{payload} = $self->copy if
1284 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1287 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1289 $self->push_events(@allevents);
1292 sub matrix_test_result_codes {
1294 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1297 sub matrix_test_result_events {
1300 my $event = new OpenILS::Event(
1301 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1303 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1305 } (@{$self->matrix_test_result});
1308 sub run_indb_circ_test {
1310 return $self->matrix_test_result if $self->matrix_test_result;
1312 my $dbfunc = ($self->is_renewal) ?
1313 'action.item_user_renew_test' : 'action.item_user_circ_test';
1315 if( $self->is_precat && $self->request_precat) {
1316 $self->make_precat_copy;
1317 return if $self->bail_out;
1320 my $results = $self->editor->json_query(
1324 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1330 $self->circ_test_success($U->is_true($results->[0]->{success}));
1332 if(my $mp = $results->[0]->{matchpoint}) {
1333 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1334 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1335 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1336 if(defined($results->[0]->{renewals})) {
1337 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1339 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1340 if(defined($results->[0]->{grace_period})) {
1341 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1343 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1344 if(defined($results->[0]->{hard_due_date})) {
1345 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1347 # Grab the *last* response for limit_groups, where it is more likely to be filled
1348 $self->limit_groups($results->[-1]->{limit_groups});
1351 return $self->matrix_test_result($results);
1354 # ---------------------------------------------------------------------
1355 # given a use and copy, this will calculate the circulation policy
1356 # parameters. Only works with in-db circ.
1357 # ---------------------------------------------------------------------
1361 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1363 $self->run_indb_circ_test;
1366 circ_test_success => $self->circ_test_success,
1367 failure_events => [],
1368 failure_codes => [],
1369 matchpoint => $self->circ_matrix_matchpoint
1372 unless($self->circ_test_success) {
1373 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1374 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1377 if($self->circ_matrix_matchpoint) {
1378 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1379 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1380 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1381 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1383 my $policy = $self->get_circ_policy(
1384 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1386 $$results{$_} = $$policy{$_} for keys %$policy;
1392 # ---------------------------------------------------------------------
1393 # Loads the circ policy info for duration, recurring fine, and max
1394 # fine based on the current copy
1395 # ---------------------------------------------------------------------
1396 sub get_circ_policy {
1397 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1400 duration_rule => $duration_rule->name,
1401 recurring_fine_rule => $recurring_fine_rule->name,
1402 max_fine_rule => $max_fine_rule->name,
1403 max_fine => $self->get_max_fine_amount($max_fine_rule),
1404 fine_interval => $recurring_fine_rule->recurrence_interval,
1405 renewal_remaining => $duration_rule->max_renewals,
1406 auto_renewal_remaining => $duration_rule->max_auto_renewals,
1407 grace_period => $recurring_fine_rule->grace_period
1410 if($hard_due_date) {
1411 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1412 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1415 $policy->{duration_date_ceiling} = undef;
1416 $policy->{duration_date_ceiling_force} = undef;
1419 $policy->{duration} = $duration_rule->shrt
1420 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1421 $policy->{duration} = $duration_rule->normal
1422 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1423 $policy->{duration} = $duration_rule->extended
1424 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1426 $policy->{recurring_fine} = $recurring_fine_rule->low
1427 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1428 $policy->{recurring_fine} = $recurring_fine_rule->normal
1429 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1430 $policy->{recurring_fine} = $recurring_fine_rule->high
1431 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1436 sub get_max_fine_amount {
1438 my $max_fine_rule = shift;
1439 my $max_amount = $max_fine_rule->amount;
1441 # if is_percent is true then the max->amount is
1442 # use as a percentage of the copy price
1443 if ($U->is_true($max_fine_rule->is_percent)) {
1444 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1445 $max_amount = $price * $max_fine_rule->amount / 100;
1447 $U->ou_ancestor_setting_value(
1449 'circ.max_fine.cap_at_price',
1453 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1454 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1462 sub run_copy_permit_scripts {
1464 my $copy = $self->copy || return;
1468 my $results = $self->run_indb_circ_test;
1469 push @allevents, $self->matrix_test_result_events
1470 unless $self->circ_test_success;
1472 # See if this copy has an alert message
1473 my $ae = $self->check_copy_alert();
1474 push( @allevents, $ae ) if $ae;
1476 # uniquify the events
1477 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1478 @allevents = values %hash;
1480 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1482 $self->push_events(@allevents);
1486 sub check_copy_alert {
1489 if ($self->new_copy_alerts) {
1491 push @alerts, @{$self->user_copy_alerts} # we have preexisting alerts
1492 if ($self->user_copy_alerts && @{$self->user_copy_alerts});
1494 push @alerts, @{$self->system_copy_alerts} # we have new dynamic alerts
1495 if ($self->system_copy_alerts && @{$self->system_copy_alerts});
1498 $self->bail_out(1) if (!$self->override);
1499 return OpenILS::Event->new( 'COPY_ALERT_MESSAGE', payload => \@alerts);
1503 return undef if $self->is_renewal;
1504 return OpenILS::Event->new(
1505 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1506 if $self->copy and $self->copy->alert_message;
1512 # --------------------------------------------------------------------------
1513 # If the call is overriding and has permissions to override every collected
1514 # event, the are cleared. Any event that the caller does not have
1515 # permission to override, will be left in the event list and bail_out will
1517 # XXX We need code in here to cancel any holds/transits on copies
1518 # that are being force-checked out
1519 # --------------------------------------------------------------------------
1520 sub override_events {
1522 my @events = @{$self->events};
1523 return unless @events;
1524 my $oargs = $self->override_args;
1526 if(!$self->override) {
1527 return $self->bail_out(1)
1528 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1533 for my $e (@events) {
1534 my $tc = $e->{textcode};
1535 next if $tc eq 'SUCCESS';
1536 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1537 my $ov = "$tc.override";
1538 $logger->info("circulator: attempting to override event: $ov");
1540 return $self->bail_on_events($self->editor->event)
1541 unless( $self->editor->allowed($ov) );
1543 return $self->bail_out(1);
1549 # --------------------------------------------------------------------------
1550 # If there is an open claimsreturn circ on the requested copy, close the
1551 # circ if overriding, otherwise bail out
1552 # --------------------------------------------------------------------------
1553 sub handle_claims_returned {
1555 my $copy = $self->copy;
1557 my $CR = $self->editor->search_action_circulation(
1559 target_copy => $copy->id,
1560 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1561 checkin_time => undef,
1565 return unless ($CR = $CR->[0]);
1569 # - If the caller has set the override flag, we will check the item in
1570 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1572 $CR->checkin_time('now');
1573 $CR->checkin_scan_time('now');
1574 $CR->checkin_lib($self->circ_lib);
1575 $CR->checkin_workstation($self->editor->requestor->wsid);
1576 $CR->checkin_staff($self->editor->requestor->id);
1578 $evt = $self->editor->event
1579 unless $self->editor->update_action_circulation($CR);
1582 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1585 $self->bail_on_events($evt) if $evt;
1590 # --------------------------------------------------------------------------
1591 # This performs the checkout
1592 # --------------------------------------------------------------------------
1596 $self->log_me("do_checkout()");
1598 # make sure perms are good if this isn't a renewal
1599 unless( $self->is_renewal ) {
1600 return $self->bail_on_events($self->editor->event)
1601 unless( $self->editor->allowed('COPY_CHECKOUT') );
1604 # verify the permit key
1605 unless( $self->check_permit_key ) {
1606 if( $self->permit_override ) {
1607 return $self->bail_on_events($self->editor->event)
1608 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1610 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1614 # if this is a non-cataloged circ, build the circ and finish
1615 if( $self->is_noncat ) {
1616 $self->checkout_noncat;
1618 OpenILS::Event->new('SUCCESS',
1619 payload => { noncat_circ => $self->circ }));
1623 if( $self->is_precat ) {
1624 $self->make_precat_copy;
1625 return if $self->bail_out;
1627 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1628 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1631 $self->do_copy_checks;
1632 return if $self->bail_out;
1634 $self->run_checkout_scripts();
1635 return if $self->bail_out;
1637 $self->build_checkout_circ_object();
1638 return if $self->bail_out;
1640 my $modify_to_start = $self->booking_adjusted_due_date();
1641 return if $self->bail_out;
1643 $self->apply_modified_due_date($modify_to_start);
1644 return if $self->bail_out;
1646 return $self->bail_on_events($self->editor->event)
1647 unless $self->editor->create_action_circulation($self->circ);
1649 # refresh the circ to force local time zone for now
1650 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1652 if($self->limit_groups) {
1653 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1656 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1658 return if $self->bail_out;
1660 $self->apply_deposit_fee();
1661 return if $self->bail_out;
1663 $self->handle_checkout_holds();
1664 return if $self->bail_out;
1666 # ------------------------------------------------------------------------------
1667 # Update the patron penalty info in the DB. Run it for permit-overrides
1668 # since the penalties are not updated during the permit phase
1669 # ------------------------------------------------------------------------------
1670 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1672 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1675 if($self->is_renewal) {
1676 # flesh the billing summary for the checked-in circ
1677 $pcirc = $self->editor->retrieve_action_circulation([
1679 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1684 OpenILS::Event->new('SUCCESS',
1686 copy => $U->unflesh_copy($self->copy),
1687 volume => $self->volume,
1688 circ => $self->circ,
1690 holds_fulfilled => $self->fulfilled_holds,
1691 deposit_billing => $self->deposit_billing,
1692 rental_billing => $self->rental_billing,
1693 parent_circ => $pcirc,
1694 patron => ($self->return_patron) ? $self->patron : undef,
1695 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1701 sub apply_deposit_fee {
1703 my $copy = $self->copy;
1705 ($self->is_deposit and not $self->is_deposit_exempt) or
1706 ($self->is_rental and not $self->is_rental_exempt);
1708 return if $self->is_deposit and $self->skip_deposit_fee;
1709 return if $self->is_rental and $self->skip_rental_fee;
1711 my $bill = Fieldmapper::money::billing->new;
1712 my $amount = $copy->deposit_amount;
1716 if($self->is_deposit) {
1717 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1719 $self->deposit_billing($bill);
1721 $billing_type = OILS_BILLING_TYPE_RENTAL;
1723 $self->rental_billing($bill);
1726 $bill->xact($self->circ->id);
1727 $bill->amount($amount);
1728 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1729 $bill->billing_type($billing_type);
1730 $bill->btype($btype);
1731 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1733 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1738 my $copy = $self->copy;
1740 my $stat = $copy->status if ref $copy->status;
1741 my $loc = $copy->location if ref $copy->location;
1742 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1744 $copy->status($stat->id) if $stat;
1745 $copy->location($loc->id) if $loc;
1746 $copy->circ_lib($circ_lib->id) if $circ_lib;
1747 $copy->editor($self->editor->requestor->id);
1748 $copy->edit_date('now');
1749 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1751 return $self->bail_on_events($self->editor->event)
1752 unless $self->editor->update_asset_copy($self->copy);
1754 $copy->status($U->copy_status($copy->status));
1755 $copy->location($loc) if $loc;
1756 $copy->circ_lib($circ_lib) if $circ_lib;
1759 sub update_reservation {
1761 my $reservation = $self->reservation;
1763 my $usr = $reservation->usr;
1764 my $target_rt = $reservation->target_resource_type;
1765 my $target_r = $reservation->target_resource;
1766 my $current_r = $reservation->current_resource;
1768 $reservation->usr($usr->id) if ref $usr;
1769 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1770 $reservation->target_resource($target_r->id) if ref $target_r;
1771 $reservation->current_resource($current_r->id) if ref $current_r;
1773 return $self->bail_on_events($self->editor->event)
1774 unless $self->editor->update_booking_reservation($self->reservation);
1777 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1778 $self->reservation($reservation);
1782 sub bail_on_events {
1783 my( $self, @evts ) = @_;
1784 $self->push_events(@evts);
1788 # ------------------------------------------------------------------------------
1789 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1790 # affects copies that will fulfill holds and CIRC affects all other copies.
1791 # If blocks exists, bail, push Events onto the event pile, and return true.
1792 # ------------------------------------------------------------------------------
1793 sub check_hold_fulfill_blocks {
1796 # With the addition of ignore_proximity in csp, we need to fetch
1797 # the proximity of both the circ_lib and the copy's circ_lib to
1798 # the patron's home_ou.
1799 my ($ou_prox, $copy_prox);
1800 my $home_ou = (ref($self->patron->home_ou)) ? $self->patron->home_ou->id : $self->patron->home_ou;
1801 $ou_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $self->circ_lib);
1802 $ou_prox = -1 unless (defined($ou_prox));
1803 my $copy_ou = (ref($self->copy->circ_lib)) ? $self->copy->circ_lib->id : $self->copy->circ_lib;
1804 if ($copy_ou == $self->circ_lib) {
1805 # Save us the time of an extra query.
1806 $copy_prox = $ou_prox;
1808 $copy_prox = $U->get_org_unit_proximity($self->editor, $home_ou, $copy_ou);
1809 $copy_prox = -1 unless (defined($copy_prox));
1812 # See if the user has any penalties applied that prevent hold fulfillment
1813 my $pens = $self->editor->json_query({
1814 select => {csp => ['name', 'label']},
1815 from => {ausp => {csp => {}}},
1818 usr => $self->patron->id,
1819 org_unit => $U->get_org_full_path($self->circ_lib),
1821 {stop_date => undef},
1822 {stop_date => {'>' => 'now'}}
1826 block_list => {'like' => '%FULFILL%'},
1828 {ignore_proximity => undef},
1829 {ignore_proximity => {'<' => $ou_prox}},
1830 {ignore_proximity => {'<' => $copy_prox}}
1836 return 0 unless @$pens;
1838 for my $pen (@$pens) {
1839 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1840 my $event = OpenILS::Event->new($pen->{name});
1841 $event->{desc} = $pen->{label};
1842 $self->push_events($event);
1845 $self->override_events;
1846 return $self->bail_out;
1850 # ------------------------------------------------------------------------------
1851 # When an item is checked out, see if we can fulfill a hold for this patron
1852 # ------------------------------------------------------------------------------
1853 sub handle_checkout_holds {
1855 my $copy = $self->copy;
1856 my $patron = $self->patron;
1858 my $e = $self->editor;
1859 $self->fulfilled_holds([]);
1861 # non-cats can't fulfill a hold
1862 return if $self->is_noncat;
1864 my $hold = $e->search_action_hold_request({
1865 current_copy => $copy->id ,
1866 cancel_time => undef,
1867 fulfillment_time => undef
1870 if($hold and $hold->usr != $patron->id) {
1871 # reset the hold since the copy is now checked out
1873 $logger->info("circulator: un-targeting hold ".$hold->id.
1874 " because copy ".$copy->id." is getting checked out");
1876 $hold->clear_prev_check_time;
1877 $hold->clear_current_copy;
1878 $hold->clear_capture_time;
1879 $hold->clear_shelf_time;
1880 $hold->clear_shelf_expire_time;
1881 $hold->clear_current_shelf_lib;
1883 return $self->bail_on_event($e->event)
1884 unless $e->update_action_hold_request($hold);
1890 $hold = $self->find_related_user_hold($copy, $patron) or return;
1891 $logger->info("circulator: found related hold to fulfill in checkout");
1894 return if $self->check_hold_fulfill_blocks;
1896 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1898 # if the hold was never officially captured, capture it.
1899 $hold->current_copy($copy->id);
1900 $hold->capture_time('now') unless $hold->capture_time;
1901 $hold->fulfillment_time('now');
1902 $hold->fulfillment_staff($e->requestor->id);
1903 $hold->fulfillment_lib($self->circ_lib);
1905 return $self->bail_on_events($e->event)
1906 unless $e->update_action_hold_request($hold);
1908 return $self->fulfilled_holds([$hold->id]);
1912 # ------------------------------------------------------------------------------
1913 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1914 # the patron directly targets the checked out item, see if there is another hold
1915 # for the patron that could be fulfilled by the checked out item. Fulfill the
1916 # oldest hold and only fulfill 1 of them.
1918 # For "another hold":
1920 # First, check for one that the copy matches via hold_copy_map, ensuring that
1921 # *any* hold type that this copy could fill may end up filled.
1923 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1924 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1925 # that are non-requestable to count as capturing those hold types.
1926 # ------------------------------------------------------------------------------
1927 sub find_related_user_hold {
1928 my($self, $copy, $patron) = @_;
1929 my $e = $self->editor;
1931 # holds on precat copies are always copy-level, so this call will
1932 # always return undef. Exit early.
1933 return undef if $self->is_precat;
1935 return undef unless $U->ou_ancestor_setting_value(
1936 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1938 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1940 select => {ahr => ['id']},
1949 fkey => 'current_copy',
1950 type => 'left' # there may be no current_copy
1957 fulfillment_time => undef,
1958 cancel_time => undef,
1960 {expire_time => undef},
1961 {expire_time => {'>' => 'now'}}
1965 target_copy => $self->copy->id
1969 {id => undef}, # left-join copy may be nonexistent
1970 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1974 order_by => {ahr => {request_time => {direction => 'asc'}}},
1978 my $hold_info = $e->json_query($args)->[0];
1979 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1980 return undef if $U->ou_ancestor_setting_value(
1981 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1983 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1985 select => {ahr => ['id']},
1990 fkey => 'current_copy',
1991 type => 'left' # there may be no current_copy
1998 fulfillment_time => undef,
1999 cancel_time => undef,
2001 {expire_time => undef},
2002 {expire_time => {'>' => 'now'}}
2009 target => $self->volume->id
2015 target => $self->title->id
2021 {id => undef}, # left-join copy may be nonexistent
2022 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
2026 order_by => {ahr => {request_time => {direction => 'asc'}}},
2030 $hold_info = $e->json_query($args)->[0];
2031 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
2036 sub run_checkout_scripts {
2049 my $hard_due_date_name;
2051 $self->run_indb_circ_test();
2052 $duration = $self->circ_matrix_matchpoint->duration_rule;
2053 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
2054 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
2055 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
2057 $duration_name = $duration->name if $duration;
2058 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
2061 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
2062 return $self->bail_on_events($evt) if ($evt && !$nobail);
2064 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
2065 return $self->bail_on_events($evt) if ($evt && !$nobail);
2067 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
2068 return $self->bail_on_events($evt) if ($evt && !$nobail);
2070 if($hard_due_date_name) {
2071 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
2072 return $self->bail_on_events($evt) if ($evt && !$nobail);
2078 # The item circulates with an unlimited duration
2082 $hard_due_date = undef;
2085 $self->duration_rule($duration);
2086 $self->recurring_fines_rule($recurring);
2087 $self->max_fine_rule($max_fine);
2088 $self->hard_due_date($hard_due_date);
2092 sub build_checkout_circ_object {
2095 my $circ = Fieldmapper::action::circulation->new;
2096 my $duration = $self->duration_rule;
2097 my $max = $self->max_fine_rule;
2098 my $recurring = $self->recurring_fines_rule;
2099 my $hard_due_date = $self->hard_due_date;
2100 my $copy = $self->copy;
2101 my $patron = $self->patron;
2102 my $duration_date_ceiling;
2103 my $duration_date_ceiling_force;
2107 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
2108 $duration_date_ceiling = $policy->{duration_date_ceiling};
2109 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
2111 my $dname = $duration->name;
2112 my $mname = $max->name;
2113 my $rname = $recurring->name;
2115 if($hard_due_date) {
2116 $hdname = $hard_due_date->name;
2119 $logger->debug("circulator: building circulation ".
2120 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
2122 $circ->duration($policy->{duration});
2123 $circ->recurring_fine($policy->{recurring_fine});
2124 $circ->duration_rule($duration->name);
2125 $circ->recurring_fine_rule($recurring->name);
2126 $circ->max_fine_rule($max->name);
2127 $circ->max_fine($policy->{max_fine});
2128 $circ->fine_interval($recurring->recurrence_interval);
2129 $circ->renewal_remaining($duration->max_renewals);
2130 $circ->auto_renewal_remaining($duration->max_auto_renewals);
2131 $circ->grace_period($policy->{grace_period});
2135 $logger->info("circulator: copy found with an unlimited circ duration");
2136 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2137 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2138 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2139 $circ->renewal_remaining(0);
2140 $circ->grace_period(0);
2143 $circ->target_copy( $copy->id );
2144 $circ->usr( $patron->id );
2145 $circ->circ_lib( $self->circ_lib );
2146 $circ->workstation($self->editor->requestor->wsid)
2147 if defined $self->editor->requestor->wsid;
2149 # renewals maintain a link to the parent circulation
2150 $circ->parent_circ($self->parent_circ);
2152 if( $self->is_renewal ) {
2153 $circ->opac_renewal('t') if $self->opac_renewal;
2154 $circ->phone_renewal('t') if $self->phone_renewal;
2155 $circ->desk_renewal('t') if $self->desk_renewal;
2156 $circ->renewal_remaining($self->renewal_remaining);
2157 $circ->circ_staff($self->editor->requestor->id);
2160 if ( $self->is_autorenewal ){
2161 $circ->auto_renewal_remaining($self->auto_renewal_remaining);
2162 $circ->auto_renewal('t');
2165 # if the user provided an overiding checkout time,
2166 # (e.g. the checkout really happened several hours ago), then
2167 # we apply that here. Does this need a perm??
2168 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2169 if $self->checkout_time;
2171 # if a patron is renewing, 'requestor' will be the patron
2172 $circ->circ_staff($self->editor->requestor->id);
2173 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force, $circ->xact_start) ) if $circ->duration;
2178 sub do_reservation_pickup {
2181 $self->log_me("do_reservation_pickup()");
2183 $self->reservation->pickup_time('now');
2186 $self->reservation->current_resource &&
2187 $U->is_true($self->reservation->target_resource_type->catalog_item)
2189 # We used to try to set $self->copy and $self->patron here,
2190 # but that should already be done.
2192 $self->run_checkout_scripts(1);
2194 my $duration = $self->duration_rule;
2195 my $max = $self->max_fine_rule;
2196 my $recurring = $self->recurring_fines_rule;
2198 if ($duration && $max && $recurring) {
2199 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2201 my $dname = $duration->name;
2202 my $mname = $max->name;
2203 my $rname = $recurring->name;
2205 $logger->debug("circulator: updating reservation ".
2206 "with duration=$dname, maxfine=$mname, recurring=$rname");
2208 $self->reservation->fine_amount($policy->{recurring_fine});
2209 $self->reservation->max_fine($policy->{max_fine});
2210 $self->reservation->fine_interval($recurring->recurrence_interval);
2213 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2214 $self->update_copy();
2217 $self->reservation->fine_amount(
2218 $self->reservation->target_resource_type->fine_amount
2220 $self->reservation->max_fine(
2221 $self->reservation->target_resource_type->max_fine
2223 $self->reservation->fine_interval(
2224 $self->reservation->target_resource_type->fine_interval
2228 $self->update_reservation();
2231 sub do_reservation_return {
2233 my $request = shift;
2235 $self->log_me("do_reservation_return()");
2237 if (not ref $self->reservation) {
2238 my ($reservation, $evt) =
2239 $U->fetch_booking_reservation($self->reservation);
2240 return $self->bail_on_events($evt) if $evt;
2241 $self->reservation($reservation);
2244 $self->handle_fines(1);
2245 $self->reservation->return_time('now');
2246 $self->update_reservation();
2247 $self->reshelve_copy if $self->copy;
2249 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2250 $self->copy( $self->reservation->current_resource->catalog_item );
2254 sub booking_adjusted_due_date {
2256 my $circ = $self->circ;
2257 my $copy = $self->copy;
2259 return undef unless $self->use_booking;
2263 if( $self->due_date ) {
2265 return $self->bail_on_events($self->editor->event)
2266 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2268 $circ->due_date(cleanse_ISO8601($self->due_date));
2272 return unless $copy and $circ->due_date;
2275 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2276 if (@$booking_items) {
2277 my $booking_item = $booking_items->[0];
2278 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2280 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2281 my $shorten_circ_setting = $resource_type->elbow_room ||
2282 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2285 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2286 my $bookings = $booking_ses->request('open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken, {
2287 resource => $booking_item->id
2288 , search_start => 'now'
2289 , search_end => $circ->due_date
2290 , fields => { cancel_time => undef, return_time => undef }
2292 $booking_ses->disconnect;
2294 throw OpenSRF::EX::ERROR ("Improper input arguments") unless defined $bookings;
2295 return $self->bail_on_events($bookings) if ref($bookings) eq 'HASH';
2297 my $dt_parser = DateTime::Format::ISO8601->new;
2298 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2300 for my $bid (@$bookings) {
2302 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2304 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2305 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2307 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2308 if ($booking_start < DateTime->now);
2311 if ($U->is_true($stop_circ_setting)) {
2312 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2314 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2315 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2318 # We set the circ duration here only to affect the logic that will
2319 # later (in a DB trigger) mangle the time part of the due date to
2320 # 11:59pm. Having any circ duration that is not a whole number of
2321 # days is enough to prevent the "correction."
2322 my $new_circ_duration = $due_date->epoch - time;
2323 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2324 $circ->duration("$new_circ_duration seconds");
2326 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2330 return $self->bail_on_events($self->editor->event)
2331 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2337 sub apply_modified_due_date {
2339 my $shift_earlier = shift;
2340 my $circ = $self->circ;
2341 my $copy = $self->copy;
2343 if( $self->due_date ) {
2345 return $self->bail_on_events($self->editor->event)
2346 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2348 $circ->due_date(cleanse_ISO8601($self->due_date));
2352 # if the due_date lands on a day when the location is closed
2353 return unless $copy and $circ->due_date;
2355 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2357 # due-date overlap should be determined by the location the item
2358 # is checked out from, not the owning or circ lib of the item
2359 my $org = $self->circ_lib;
2361 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2362 " with an item due date of ".$circ->due_date );
2364 my $dateinfo = $U->storagereq(
2365 'open-ils.storage.actor.org_unit.closed_date.overlap',
2366 $org, $circ->due_date );
2369 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2370 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2372 # XXX make the behavior more dynamic
2373 # for now, we just push the due date to after the close date
2374 if ($shift_earlier) {
2375 $circ->due_date($dateinfo->{start});
2377 $circ->due_date($dateinfo->{end});
2385 sub create_due_date {
2386 my( $self, $duration, $date_ceiling, $force_date, $start_time ) = @_;
2388 # if there is a raw time component (e.g. from postgres),
2389 # turn it into an interval that interval_to_seconds can parse
2390 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2392 # for now, use the server timezone. TODO: use workstation org timezone
2393 my $due_date = DateTime->now(time_zone => 'local');
2394 $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
2396 # add the circ duration
2397 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2400 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2401 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2402 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2407 # return ISO8601 time with timezone
2408 return $due_date->strftime('%FT%T%z');
2413 sub make_precat_copy {
2415 my $copy = $self->copy;
2418 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2420 $copy->editor($self->editor->requestor->id);
2421 $copy->edit_date('now');
2422 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2423 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2424 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2425 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2426 $self->update_copy();
2430 $logger->info("circulator: Creating a new precataloged ".
2431 "copy in checkout with barcode " . $self->copy_barcode);
2433 $copy = Fieldmapper::asset::copy->new;
2434 $copy->circ_lib($self->circ_lib);
2435 $copy->creator($self->editor->requestor->id);
2436 $copy->editor($self->editor->requestor->id);
2437 $copy->barcode($self->copy_barcode);
2438 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2439 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2440 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2442 $copy->dummy_title($self->dummy_title || "");
2443 $copy->dummy_author($self->dummy_author || "");
2444 $copy->dummy_isbn($self->dummy_isbn || "");
2445 $copy->circ_modifier($self->circ_modifier);
2448 # See if we need to override the circ_lib for the copy with a configured circ_lib
2449 # Setting is shortname of the org unit
2450 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2451 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2453 if($precat_circ_lib) {
2454 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2457 $self->bail_on_events($self->editor->event);
2461 $copy->circ_lib($org->id);
2465 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2467 $self->push_events($self->editor->event);
2473 sub checkout_noncat {
2479 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2480 my $count = $self->noncat_count || 1;
2481 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2483 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2487 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2488 $self->editor->requestor->id,
2496 $self->push_events($evt);
2504 # If a copy goes into transit and is then checked in before the transit checkin
2505 # interval has expired, push an event onto the overridable events list.
2506 sub check_transit_checkin_interval {
2509 # only concerned with in-transit items
2510 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2512 # no interval, no problem
2513 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2514 return unless $interval;
2516 # capture the transit so we don't have to fetch it again later during checkin
2518 $self->editor->search_action_transit_copy(
2519 {target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef}
2523 # transit from X to X for whatever reason has no min interval
2524 return if $self->transit->source == $self->transit->dest;
2526 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2527 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2528 my $horizon = $t_start->add(seconds => $seconds);
2530 # See if we are still within the transit checkin forbidden range
2531 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2532 if $horizon > DateTime->now;
2535 # Retarget local holds at checkin
2536 sub checkin_retarget {
2538 return unless $self->retarget_mode and $self->retarget_mode =~ m/retarget/; # Retargeting?
2539 return unless $self->is_checkin; # Renewals need not be checked
2540 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2541 return if $self->is_precat; # No holds for precats
2542 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2543 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2544 my $status = $U->copy_status($self->copy->status);
2545 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2546 # Specifically target items that are likely new (by status ID)
2547 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2548 my $location = $self->copy->location;
2549 if(!ref($location)) {
2550 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2551 $self->copy->location($location);
2553 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2555 # Fetch holds for the bib
2556 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2557 $self->editor->authtoken,
2560 capture_time => undef, # No touching captured holds
2561 frozen => 'f', # Don't bother with frozen holds
2562 pickup_lib => $self->circ_lib # Only holds actually here
2565 # Error? Skip the step.
2566 return if exists $result->{"ilsevent"};
2570 foreach my $holdlist (keys %{$result}) {
2571 push @$holds, @{$result->{$holdlist}};
2574 return if scalar(@$holds) == 0; # No holds, no retargeting
2576 # Check for parts on this copy
2577 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2578 my %parts_hash = ();
2579 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2581 # Loop over holds in request-ish order
2582 # Stage 1: Get them into request-ish order
2583 # Also grab type and target for skipping low hanging ones
2584 $result = $self->editor->json_query({
2585 "select" => { "ahr" => ["id", "hold_type", "target"] },
2586 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2587 "where" => { "id" => $holds },
2589 { "class" => "pgt", "field" => "hold_priority"},
2590 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2591 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2592 { "class" => "ahr", "field" => "request_time"}
2597 if (ref $result eq "ARRAY" and scalar @$result) {
2598 foreach (@{$result}) {
2599 # Copy level, but not this copy?
2600 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2601 and $_->{target} != $self->copy->id);
2602 # Volume level, but not this volume?
2603 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2604 if(@$parts) { # We have parts?
2606 next if ($_->{hold_type} eq 'T');
2607 # Skip part holds for parts not on this copy
2608 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2610 # No parts, no part holds
2611 next if ($_->{hold_type} eq 'P');
2613 # So much for easy stuff, attempt a retarget!
2614 my $tresult = $U->simplereq(
2615 'open-ils.hold-targeter',
2616 'open-ils.hold-targeter.target',
2617 {hold => $_->{id}, find_copy => $self->copy->id}
2619 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2620 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2628 $self->log_me("do_checkin()");
2630 return $self->bail_on_events(
2631 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2634 $self->check_transit_checkin_interval;
2635 $self->checkin_retarget;
2637 # the renew code and mk_env should have already found our circulation object
2638 unless( $self->circ ) {
2640 my $circs = $self->editor->search_action_circulation(
2641 { target_copy => $self->copy->id, checkin_time => undef });
2643 $self->circ($$circs[0]);
2645 # for now, just warn if there are multiple open circs on a copy
2646 $logger->warn("circulator: we have ".scalar(@$circs).
2647 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2650 my $stat = $U->copy_status($self->copy->status)->id;
2652 # LOST (and to some extent, LONGOVERDUE) may optionally be handled
2653 # differently if they are already paid for. We need to check for this
2654 # early since overdue generation is potentially affected.
2655 my $dont_change_lost_zero = 0;
2656 if ($stat == OILS_COPY_STATUS_LOST
2657 || $stat == OILS_COPY_STATUS_LOST_AND_PAID
2658 || $stat == OILS_COPY_STATUS_LONG_OVERDUE) {
2660 # LOST fine settings are controlled by the copy's circ lib, not the the
2662 my $copy_circ_lib = (ref $self->copy->circ_lib) ?
2663 $self->copy->circ_lib->id : $self->copy->circ_lib;
2664 $dont_change_lost_zero = $U->ou_ancestor_setting_value(
2665 $copy_circ_lib, 'circ.checkin.lost_zero_balance.do_not_change',
2666 $self->editor) || 0;
2668 if ($dont_change_lost_zero) {
2669 my ($obt) = $U->fetch_mbts($self->circ->id, $self->editor);
2670 $dont_change_lost_zero = 0 if( $obt and $obt->balance_owed != 0 );
2673 $self->dont_change_lost_zero($dont_change_lost_zero);
2676 if( $self->checkin_check_holds_shelf() ) {
2677 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2678 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2679 if($self->fake_hold_dest) {
2680 $self->hold->pickup_lib($self->circ_lib);
2682 $self->checkin_flesh_events;
2686 unless( $self->is_renewal ) {
2687 return $self->bail_on_events($self->editor->event)
2688 unless $self->editor->allowed('COPY_CHECKIN');
2691 $self->push_events($self->check_copy_alert());
2692 $self->push_events($self->check_checkin_copy_status());
2694 # if the circ is marked as 'claims returned', add the event to the list
2695 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2696 if ($self->circ and $self->circ->stop_fines
2697 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2699 $self->check_circ_deposit();
2701 # handle the overridable events
2702 $self->override_events unless $self->is_renewal;
2703 return if $self->bail_out;
2705 if( $self->copy and !$self->transit ) {
2707 $self->editor->search_action_transit_copy(
2708 { target_copy => $self->copy->id, dest_recv_time => undef, cancel_time => undef }
2714 $self->checkin_handle_circ_start;
2715 return if $self->bail_out;
2717 if (!$dont_change_lost_zero) {
2718 # if this circ is LOST and we are configured to generate overdue
2719 # fines for lost items on checkin (to fill the gap between mark
2720 # lost time and when the fines would have naturally stopped), then
2721 # stop_fines is no longer valid and should be cleared.
2723 # stop_fines will be set again during the handle_fines() stage.
2724 # XXX should this setting come from the copy circ lib (like other
2725 # LOST settings), instead of the circulation circ lib?
2726 if ($stat == OILS_COPY_STATUS_LOST) {
2727 $self->circ->clear_stop_fines if
2728 $U->ou_ancestor_setting_value(
2730 OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN,
2735 # Set stop_fines when claimed never checked out
2736 $self->circ->stop_fines( OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT ) if( $self->claims_never_checked_out );
2738 # handle fines for this circ, including overdue gen if needed
2739 $self->handle_fines;
2742 $self->checkin_handle_circ_finish;
2743 return if $self->bail_out;
2744 $self->checkin_changed(1);
2746 } elsif( $self->transit ) {
2747 my $hold_transit = $self->process_received_transit;
2748 $self->checkin_changed(1);
2750 if( $self->bail_out ) {
2751 $self->checkin_flesh_events;
2755 if( my $e = $self->check_checkin_copy_status() ) {
2756 # If the original copy status is special, alert the caller
2757 my $ev = $self->events;
2758 $self->events([$e]);
2759 $self->override_events;
2760 return if $self->bail_out;
2764 if( $hold_transit or
2765 $U->copy_status($self->copy->status)->id
2766 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2769 if( $hold_transit ) {
2770 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2772 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2777 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2779 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2780 $self->reshelve_copy(1);
2781 $self->cancelled_hold_transit(1);
2782 $self->notify_hold(0); # don't notify for cancelled holds
2783 $self->fake_hold_dest(0);
2784 return if $self->bail_out;
2786 } elsif ($hold and $hold->hold_type eq 'R') {
2788 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2789 $self->notify_hold(0); # No need to notify
2790 $self->fake_hold_dest(0);
2791 $self->noop(1); # Don't try and capture for other holds/transits now
2792 $self->update_copy();
2793 $hold->fulfillment_time('now');
2794 $self->bail_on_events($self->editor->event)
2795 unless $self->editor->update_action_hold_request($hold);
2799 # hold transited to correct location
2800 if($self->fake_hold_dest) {
2801 $hold->pickup_lib($self->circ_lib);
2803 $self->checkin_flesh_events;
2808 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2810 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2811 " that is in-transit, but there is no transit.. repairing");
2812 $self->reshelve_copy(1);
2813 return if $self->bail_out;
2816 if( $self->is_renewal ) {
2817 $self->finish_fines_and_voiding;
2818 return if $self->bail_out;
2819 $self->push_events(OpenILS::Event->new('SUCCESS'));
2823 # ------------------------------------------------------------------------------
2824 # Circulations and transits are now closed where necessary. Now go on to see if
2825 # this copy can fulfill a hold or needs to be routed to a different location
2826 # ------------------------------------------------------------------------------
2828 my $needed_for_something = 0; # formerly "needed_for_hold"
2830 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2832 if (!$self->remote_hold) {
2833 if ($self->use_booking) {
2834 my $potential_hold = $self->hold_capture_is_possible;
2835 my $potential_reservation = $self->reservation_capture_is_possible;
2837 if ($potential_hold and $potential_reservation) {
2838 $logger->info("circulator: item could fulfill either hold or reservation");
2839 $self->push_events(new OpenILS::Event(
2840 "HOLD_RESERVATION_CONFLICT",
2841 "hold" => $potential_hold,
2842 "reservation" => $potential_reservation
2844 return if $self->bail_out;
2845 } elsif ($potential_hold) {
2846 $needed_for_something =
2847 $self->attempt_checkin_hold_capture;
2848 } elsif ($potential_reservation) {
2849 $needed_for_something =
2850 $self->attempt_checkin_reservation_capture;
2853 $needed_for_something = $self->attempt_checkin_hold_capture;
2856 return if $self->bail_out;
2858 unless($needed_for_something) {
2859 my $circ_lib = (ref $self->copy->circ_lib) ?
2860 $self->copy->circ_lib->id : $self->copy->circ_lib;
2862 if( $self->remote_hold ) {
2863 $circ_lib = $self->remote_hold->pickup_lib;
2864 $logger->warn("circulator: Copy ".$self->copy->barcode.
2865 " is on a remote hold's shelf, sending to $circ_lib");
2868 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2870 my $suppress_transit = 0;
2872 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2873 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2874 if($suppress_transit_source && $suppress_transit_source->{value}) {
2875 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2876 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2877 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2878 $suppress_transit = 1;
2883 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2884 # copy is where it needs to be, either for hold or reshelving
2886 $self->checkin_handle_precat();
2887 return if $self->bail_out;
2890 # copy needs to transit "home", or stick here if it's a floating copy
2892 if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
2893 my $res = $self->editor->json_query(
2895 'evergreen.can_float',
2896 $self->copy->floating->id,
2897 $self->copy->circ_lib,
2902 $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
2904 if ($can_float) { # Yep, floating, stick here
2905 $self->checkin_changed(1);
2906 $self->copy->circ_lib( $self->circ_lib );
2909 my $bc = $self->copy->barcode;
2910 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2911 $self->checkin_build_copy_transit($circ_lib);
2912 return if $self->bail_out;
2913 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2917 } else { # no-op checkin
2918 if ($self->copy->floating) { # XXX floating items still stick where they are even with no-op checkin?
2919 my $res = $self->editor->json_query(
2922 'evergreen.can_float',
2923 $self->copy->floating->id,
2924 $self->copy->circ_lib,
2929 if ($res && @$res && $U->is_true($res->[0]->{'evergreen.can_float'})) {
2930 $self->checkin_changed(1);
2931 $self->copy->circ_lib( $self->circ_lib );
2937 if($self->claims_never_checked_out and
2938 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2940 # the item was not supposed to be checked out to the user and should now be marked as missing
2941 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_MISSING;
2942 $self->copy->status($next_status);
2946 $self->reshelve_copy unless $needed_for_something;
2949 return if $self->bail_out;
2951 unless($self->checkin_changed) {
2953 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2954 my $stat = $U->copy_status($self->copy->status)->id;
2956 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2957 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2958 $self->bail_out(1); # no need to commit anything
2962 $self->push_events(OpenILS::Event->new('SUCCESS'))
2963 unless @{$self->events};
2966 $self->finish_fines_and_voiding;
2968 OpenILS::Utils::Penalty->calculate_penalties(
2969 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2971 $self->checkin_flesh_events;
2975 sub finish_fines_and_voiding {
2977 return unless $self->circ;
2979 return unless $self->backdate or $self->void_overdues;
2981 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2982 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2984 my $evt = $CC->void_or_zero_overdues(
2985 $self->editor, $self->circ, {backdate => $self->void_overdues ? undef : $self->backdate, note => $note});
2987 return $self->bail_on_events($evt) if $evt;
2989 # Make sure the circ is open or closed as necessary.
2990 $evt = $U->check_open_xact($self->editor, $self->circ->id);
2991 return $self->bail_on_events($evt) if $evt;
2997 # if a deposit was payed for this item, push the event
2998 sub check_circ_deposit {
3000 return unless $self->circ;
3001 my $deposit = $self->editor->search_money_billing(
3003 xact => $self->circ->id,
3005 }, {idlist => 1})->[0];
3007 $self->push_events(OpenILS::Event->new(
3008 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
3013 my $force = $self->force || shift;
3014 my $copy = $self->copy;
3016 my $stat = $U->copy_status($copy->status)->id;
3018 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3021 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
3022 $stat != OILS_COPY_STATUS_CATALOGING and
3023 $stat != OILS_COPY_STATUS_IN_TRANSIT and
3024 $stat != $next_status )) {
3026 $copy->status( $next_status );
3028 $self->checkin_changed(1);
3033 # Returns true if the item is at the current location
3034 # because it was transited there for a hold and the
3035 # hold has not been fulfilled
3036 sub checkin_check_holds_shelf {
3038 return 0 unless $self->copy;
3041 $U->copy_status($self->copy->status)->id ==
3042 OILS_COPY_STATUS_ON_HOLDS_SHELF;
3044 # Attempt to clear shelf expired holds for this copy
3045 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
3046 if($self->clear_expired);
3048 # find the hold that put us on the holds shelf
3049 my $holds = $self->editor->search_action_hold_request(
3051 current_copy => $self->copy->id,
3052 capture_time => { '!=' => undef },
3053 fulfillment_time => undef,
3054 cancel_time => undef,
3059 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
3060 $self->reshelve_copy(1);
3064 my $hold = $$holds[0];
3066 $logger->info("circulator: we found a captured, un-fulfilled hold [".
3067 $hold->id. "] for copy ".$self->copy->barcode);
3069 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3070 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3071 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3072 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3073 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3074 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
3075 $self->fake_hold_dest(1);
3081 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
3082 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
3086 $logger->info("circulator: hold is not for here..");
3087 $self->remote_hold($hold);
3092 sub checkin_handle_precat {
3094 my $copy = $self->copy;
3096 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
3097 $copy->status(OILS_COPY_STATUS_CATALOGING);
3098 $self->update_copy();
3099 $self->checkin_changed(1);
3100 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
3105 sub checkin_build_copy_transit {
3108 my $copy = $self->copy;
3109 my $transit = Fieldmapper::action::transit_copy->new;
3111 # if we are transiting an item to the shelf shelf, it's a hold transit
3112 if (my $hold = $self->remote_hold) {
3113 $transit = Fieldmapper::action::hold_transit_copy->new;
3114 $transit->hold($hold->id);
3116 # the item is going into transit, remove any shelf-iness
3117 if ($hold->current_shelf_lib or $hold->shelf_time) {
3118 $hold->clear_current_shelf_lib;
3119 $hold->clear_shelf_time;
3120 return $self->bail_on_events($self->editor->event)
3121 unless $self->editor->update_action_hold_request($hold);
3125 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
3126 $logger->info("circulator: transiting copy to $dest");
3128 $transit->source($self->circ_lib);
3129 $transit->dest($dest);
3130 $transit->target_copy($copy->id);
3131 $transit->source_send_time('now');
3132 $transit->copy_status( $U->copy_status($copy->status)->id );
3134 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
3136 if ($self->remote_hold) {
3137 return $self->bail_on_events($self->editor->event)
3138 unless $self->editor->create_action_hold_transit_copy($transit);
3140 return $self->bail_on_events($self->editor->event)
3141 unless $self->editor->create_action_transit_copy($transit);
3144 # ensure the transit is returned to the caller
3145 $self->transit($transit);
3147 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3149 $self->checkin_changed(1);
3153 sub hold_capture_is_possible {
3155 my $copy = $self->copy;
3157 # we've been explicitly told not to capture any holds
3158 return 0 if $self->capture eq 'nocapture';
3160 # See if this copy can fulfill any holds
3161 my $hold = $holdcode->find_nearest_permitted_hold(
3162 $self->editor, $copy, $self->editor->requestor, 1 # check_only
3164 return undef if ref $hold eq "HASH" and
3165 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
3169 sub reservation_capture_is_possible {
3171 my $copy = $self->copy;
3173 # we've been explicitly told not to capture any holds
3174 return 0 if $self->capture eq 'nocapture';
3176 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3177 my $resv = $booking_ses->request(
3178 "open-ils.booking.reservations.could_capture",
3179 $self->editor->authtoken, $copy->barcode
3181 $booking_ses->disconnect;
3182 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
3183 $self->push_events($resv);
3189 # returns true if the item was used (or may potentially be used
3190 # in subsequent calls) to capture a hold.
3191 sub attempt_checkin_hold_capture {
3193 my $copy = $self->copy;
3195 # we've been explicitly told not to capture any holds
3196 return 0 if $self->capture eq 'nocapture';
3198 # See if this copy can fulfill any holds
3199 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3200 $self->editor, $copy, $self->editor->requestor );
3203 $logger->debug("circulator: no potential permitted".
3204 "holds found for copy ".$copy->barcode);
3208 if($self->capture ne 'capture') {
3209 # see if this item is in a hold-capture-delay location
3210 my $location = $self->copy->location;
3211 if(!ref($location)) {
3212 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3213 $self->copy->location($location);
3215 if($U->is_true($location->hold_verify)) {
3216 $self->bail_on_events(
3217 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3222 $self->retarget($retarget);
3224 my $suppress_transit = 0;
3225 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3226 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3227 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3228 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3229 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3230 $suppress_transit = 1;
3231 $hold->pickup_lib($self->circ_lib);
3236 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3238 $hold->current_copy($copy->id);
3239 $hold->capture_time('now');
3240 $self->put_hold_on_shelf($hold)
3241 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3243 # prevent DB errors caused by fetching
3244 # holds from storage, and updating through cstore
3245 $hold->clear_fulfillment_time;
3246 $hold->clear_fulfillment_staff;
3247 $hold->clear_fulfillment_lib;
3248 $hold->clear_expire_time;
3249 $hold->clear_cancel_time;
3250 $hold->clear_prev_check_time unless $hold->prev_check_time;
3252 $self->bail_on_events($self->editor->event)
3253 unless $self->editor->update_action_hold_request($hold);
3255 $self->checkin_changed(1);
3257 return 0 if $self->bail_out;
3259 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3261 if ($hold->hold_type eq 'R') {
3262 $copy->status(OILS_COPY_STATUS_CATALOGING);
3263 $hold->fulfillment_time('now');
3264 $self->noop(1); # Block other transit/hold checks
3265 $self->bail_on_events($self->editor->event)
3266 unless $self->editor->update_action_hold_request($hold);
3268 # This hold was captured in the correct location
3269 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3270 $self->push_events(OpenILS::Event->new('SUCCESS'));
3272 #$self->do_hold_notify($hold->id);
3273 $self->notify_hold($hold->id);
3278 # Hold needs to be picked up elsewhere. Build a hold
3279 # transit and route the item.
3280 $self->checkin_build_hold_transit();
3281 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3282 return 0 if $self->bail_out;
3283 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3286 # make sure we save the copy status
3288 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3292 sub attempt_checkin_reservation_capture {
3294 my $copy = $self->copy;
3296 # we've been explicitly told not to capture any holds
3297 return 0 if $self->capture eq 'nocapture';
3299 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3300 my $evt = $booking_ses->request(
3301 "open-ils.booking.resources.capture_for_reservation",
3302 $self->editor->authtoken,
3304 1 # don't update copy - we probably have it locked
3306 $booking_ses->disconnect;
3308 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3310 "open-ils.booking.resources.capture_for_reservation " .
3311 "didn't return an event!"
3315 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3316 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3318 # not-transferable is an error event we'll pass on the user
3319 $logger->warn("reservation capture attempted against non-transferable item");
3320 $self->push_events($evt);
3322 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3323 # Re-retrieve copy as reservation capture may have changed
3324 # its status and whatnot.
3326 "circulator: booking capture win on copy " . $self->copy->id
3328 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3330 "circulator: changing copy " . $self->copy->id .
3331 "'s status from " . $self->copy->status . " to " .
3334 $self->copy->status($new_copy_status);
3337 $self->reservation($evt->{"payload"}->{"reservation"});
3339 if (exists $evt->{"payload"}->{"transit"}) {
3343 "org" => $evt->{"payload"}->{"transit"}->dest
3347 $self->checkin_changed(1);
3351 # other results are treated as "nothing to capture"
3355 sub do_hold_notify {
3356 my( $self, $holdid ) = @_;
3358 my $e = new_editor(xact => 1);
3359 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3361 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3362 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3364 $logger->info("circulator: running delayed hold notify process");
3366 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3367 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3369 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3370 hold_id => $holdid, requestor => $self->editor->requestor);
3372 $logger->debug("circulator: built hold notifier");
3374 if(!$notifier->event) {
3376 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3378 my $stat = $notifier->send_email_notify;
3379 if( $stat == '1' ) {
3380 $logger->info("circulator: hold notify succeeded for hold $holdid");
3384 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3387 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3391 sub retarget_holds {
3393 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3394 my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
3395 $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
3396 # no reason to wait for the return value
3400 sub checkin_build_hold_transit {
3403 my $copy = $self->copy;
3404 my $hold = $self->hold;
3405 my $trans = Fieldmapper::action::hold_transit_copy->new;
3407 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3409 $trans->hold($hold->id);
3410 $trans->source($self->circ_lib);
3411 $trans->dest($hold->pickup_lib);
3412 $trans->source_send_time("now");
3413 $trans->target_copy($copy->id);
3415 # when the copy gets to its destination, it will recover
3416 # this status - put it onto the holds shelf
3417 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3419 return $self->bail_on_events($self->editor->event)
3420 unless $self->editor->create_action_hold_transit_copy($trans);
3425 sub process_received_transit {
3427 my $copy = $self->copy;
3428 my $copyid = $self->copy->id;
3430 my $status_name = $U->copy_status($copy->status)->name;
3431 $logger->debug("circulator: attempting transit receive on ".
3432 "copy $copyid. Copy status is $status_name");
3434 my $transit = $self->transit;
3436 # Check if we are in a transit suppress range
3437 my $suppress_transit = 0;
3438 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3439 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3440 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3441 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3442 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3443 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3444 $suppress_transit = 1;
3445 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3449 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3450 # - this item is in-transit to a different location
3451 # - Or we are capturing holds as transits, so why create a new transit?
3453 my $tid = $transit->id;
3454 my $loc = $self->circ_lib;
3455 my $dest = $transit->dest;
3457 $logger->info("circulator: Fowarding transit on copy which is destined ".
3458 "for a different location. transit=$tid, copy=$copyid, current ".
3459 "location=$loc, destination location=$dest");
3461 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3463 # grab the associated hold object if available
3464 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3465 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3467 return $self->bail_on_events($evt);
3470 # The transit is received, set the receive time
3471 $transit->dest_recv_time('now');
3472 $self->bail_on_events($self->editor->event)
3473 unless $self->editor->update_action_transit_copy($transit);
3475 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3477 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3478 $copy->status( $transit->copy_status );
3479 $self->update_copy();
3480 return if $self->bail_out;
3484 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3487 # hold has arrived at destination, set shelf time
3488 $self->put_hold_on_shelf($hold);
3489 $self->bail_on_events($self->editor->event)
3490 unless $self->editor->update_action_hold_request($hold);
3491 return if $self->bail_out;
3493 $self->notify_hold($hold_transit->hold);
3496 $hold_transit = undef;
3497 $self->cancelled_hold_transit(1);
3498 $self->reshelve_copy(1);
3499 $self->fake_hold_dest(0);
3504 OpenILS::Event->new(
3507 payload => { transit => $transit, holdtransit => $hold_transit } ));
3509 return $hold_transit;
3513 # ------------------------------------------------------------------
3514 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3515 # ------------------------------------------------------------------
3516 sub put_hold_on_shelf {
3517 my($self, $hold) = @_;
3518 $hold->shelf_time('now');
3519 $hold->current_shelf_lib($self->circ_lib);
3520 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3526 my $reservation = shift;
3527 my $dt_parser = DateTime::Format::ISO8601->new;
3529 my $obj = $reservation ? $self->reservation : $self->circ;
3531 my $lost_bill_opts = $self->lost_bill_options;
3532 my $circ_lib = $lost_bill_opts->{circ_lib} if $lost_bill_opts;
3533 # first, restore any voided overdues for lost, if needed
3534 if ($self->needs_lost_bill_handling and !$self->void_overdues) {
3535 my $restore_od = $U->ou_ancestor_setting_value(
3536 $circ_lib, $lost_bill_opts->{ous_restore_overdue},
3537 $self->editor) || 0;
3538 $self->checkin_handle_lost_or_lo_now_found_restore_od($circ_lib)
3542 # next, handle normal overdue generation and apply stop_fines
3543 # XXX reservations don't have stop_fines
3544 # TODO revisit booking_reservation re: stop_fines support
3545 if ($reservation or !$obj->stop_fines) {
3548 # This is a crude check for whether we are in a grace period. The code
3549 # in generate_fines() does a more thorough job, so this exists solely
3550 # as a small optimization, and might be better off removed.
3552 # If we have a grace period
3553 if($obj->can('grace_period')) {
3554 # Parse out the due date
3555 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3556 # Add the grace period to the due date
3557 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3558 # Don't generate fines on circs still in grace period
3559 $skip_for_grace = $due_date > DateTime->now;
3561 $CC->generate_fines({circs => [$obj], editor => $self->editor})
3562 unless $skip_for_grace;
3564 if (!$reservation and !$obj->stop_fines) {
3565 $obj->stop_fines(OILS_STOP_FINES_CHECKIN);
3566 $obj->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3567 $obj->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3568 $obj->stop_fines_time('now');
3569 $obj->stop_fines_time($self->backdate) if $self->backdate;
3570 $self->editor->update_action_circulation($obj);
3574 # finally, handle voiding of lost item and processing fees
3575 if ($self->needs_lost_bill_handling) {
3576 my $void_cost = $U->ou_ancestor_setting_value(
3577 $circ_lib, $lost_bill_opts->{ous_void_item_cost},
3578 $self->editor) || 0;
3579 my $void_proc_fee = $U->ou_ancestor_setting_value(
3580 $circ_lib, $lost_bill_opts->{ous_void_proc_fee},
3581 $self->editor) || 0;
3582 $self->checkin_handle_lost_or_lo_now_found(
3583 $lost_bill_opts->{void_cost_btype},
3584 $lost_bill_opts->{is_longoverdue}) if $void_cost;
3585 $self->checkin_handle_lost_or_lo_now_found(
3586 $lost_bill_opts->{void_fee_btype},
3587 $lost_bill_opts->{is_longoverdue}) if $void_proc_fee;
3593 sub checkin_handle_circ_start {
3595 my $circ = $self->circ;
3596 my $copy = $self->copy;
3600 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3602 # backdate the circ if necessary
3603 if($self->backdate) {
3604 my $evt = $self->checkin_handle_backdate;
3605 return $self->bail_on_events($evt) if $evt;
3608 # Set the checkin vars since we have the item
3609 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3611 # capture the true scan time for back-dated checkins
3612 $circ->checkin_scan_time('now');
3614 $circ->checkin_staff($self->editor->requestor->id);
3615 $circ->checkin_lib($self->circ_lib);
3616 $circ->checkin_workstation($self->editor->requestor->wsid);
3618 my $circ_lib = (ref $self->copy->circ_lib) ?
3619 $self->copy->circ_lib->id : $self->copy->circ_lib;
3620 my $stat = $U->copy_status($self->copy->status)->id;
3622 if ($stat == OILS_COPY_STATUS_LOST || $stat == OILS_COPY_STATUS_LOST_AND_PAID) {
3623 # we will now handle lost fines, but the copy will retain its 'lost'
3624 # status if it needs to transit home unless lost_immediately_available
3627 # if we decide to also delay fine handling until the item arrives home,
3628 # we will need to call lost fine handling code both when checking items
3629 # in and also when receiving transits
3630 $self->checkin_handle_lost($circ_lib);
3631 } elsif ($stat == OILS_COPY_STATUS_LONG_OVERDUE) {
3632 # same process as above.
3633 $self->checkin_handle_long_overdue($circ_lib);
3634 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3635 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3637 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3638 $self->copy->status($U->copy_status($next_status));
3645 sub checkin_handle_circ_finish {
3647 my $e = $self->editor;
3648 my $circ = $self->circ;
3650 # Do one last check before the final circulation update to see
3651 # if the xact_finish value should be set or not.
3653 # The underlying money.billable_xact may have been updated to
3654 # reflect a change in xact_finish during checkin bills handling,
3655 # however we can't simply refresh the circulation from the DB,
3656 # because other changes may be pending. Instead, reproduce the
3657 # xact_finish check here. It won't hurt to do it again.
3659 my $sum = $e->retrieve_money_billable_transaction_summary($circ->id);
3660 if ($sum) { # is this test still needed?
3662 my $balance = $sum->balance_owed;
3664 if ($balance == 0) {
3665 $circ->xact_finish('now');
3667 $circ->clear_xact_finish;
3670 $logger->info("circulator: $balance is owed on this circulation");
3673 return $self->bail_on_events($e->event)
3674 unless $e->update_action_circulation($circ);
3679 # ------------------------------------------------------------------
3680 # See if we need to void billings, etc. for lost checkin
3681 # ------------------------------------------------------------------
3682 sub checkin_handle_lost {
3684 my $circ_lib = shift;
3686 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3687 OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3689 $self->lost_bill_options({
3690 circ_lib => $circ_lib,
3691 ous_void_item_cost => OILS_SETTING_VOID_LOST_ON_CHECKIN,
3692 ous_void_proc_fee => OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN,
3693 ous_restore_overdue => OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN,
3694 void_cost_btype => 3,
3698 return $self->checkin_handle_lost_or_longoverdue(
3699 circ_lib => $circ_lib,
3700 max_return => $max_return,
3701 ous_immediately_available => OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE,
3702 ous_use_last_activity => undef # not supported for LOST checkin
3706 # ------------------------------------------------------------------
3707 # See if we need to void billings, etc. for long-overdue checkin
3708 # note: not using constants below since they serve little purpose
3709 # for single-use strings that are descriptive in their own right
3710 # and mostly just complicate debugging.
3711 # ------------------------------------------------------------------
3712 sub checkin_handle_long_overdue {
3714 my $circ_lib = shift;
3716 $logger->info("circulator: processing long-overdue checkin...");
3718 my $max_return = $U->ou_ancestor_setting_value($circ_lib,
3719 'circ.max_accept_return_of_longoverdue', $self->editor) || 0;
3721 $self->lost_bill_options({
3722 circ_lib => $circ_lib,
3723 ous_void_item_cost => 'circ.void_longoverdue_on_checkin',
3724 ous_void_proc_fee => 'circ.void_longoverdue_proc_fee_on_checkin',
3725 is_longoverdue => 1,
3726 ous_restore_overdue => 'circ.restore_overdue_on_longoverdue_return',
3727 void_cost_btype => 10,
3728 void_fee_btype => 11
3731 return $self->checkin_handle_lost_or_longoverdue(
3732 circ_lib => $circ_lib,
3733 max_return => $max_return,
3734 ous_immediately_available => 'circ.longoverdue_immediately_available',
3735 ous_use_last_activity =>
3736 'circ.longoverdue.use_last_activity_date_on_return'
3740 # last billing activity is last payment time, last billing time, or the
3741 # circ due date. If the relevant "use last activity" org unit setting is
3742 # false/unset, then last billing activity is always the due date.
3743 sub get_circ_last_billing_activity {
3745 my $circ_lib = shift;
3746 my $setting = shift;
3747 my $date = $self->circ->due_date;
3749 return $date unless $setting and
3750 $U->ou_ancestor_setting_value($circ_lib, $setting, $self->editor);
3752 my $xact = $self->editor->retrieve_money_billable_transaction([
3754 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3757 if ($xact->summary) {
3758 $date = $xact->summary->last_payment_ts ||
3759 $xact->summary->last_billing_ts ||
3760 $self->circ->due_date;
3767 sub checkin_handle_lost_or_longoverdue {
3768 my ($self, %args) = @_;
3770 my $circ = $self->circ;
3771 my $max_return = $args{max_return};
3772 my $circ_lib = $args{circ_lib};
3777 $self->get_circ_last_billing_activity(
3778 $circ_lib, $args{ous_use_last_activity});
3781 my @tm = reverse($last_activity =~ /([\d\.]+)/og);
3782 $tm[5] -= 1 if $tm[5] > 0;
3783 my $due = timelocal(int($tm[1]), int($tm[2]),
3784 int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3787 OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3789 $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
3790 "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
3791 "DUE: $due LAST: $last_chance");
3793 $max_return = 0 if $today < $last_chance;
3799 $logger->info("circulator: check-in of lost/lo item exceeds max ".
3800 "return interval. skipping fine/fee voiding, etc.");
3802 } elsif ($self->dont_change_lost_zero) { # we leave lost zero balance alone
3804 $logger->info("circulator: check-in of lost/lo item having a balance ".
3805 "of zero, skipping fine/fee voiding and reinstatement.");
3807 } else { # within max-return interval or no interval defined
3809 $logger->info("circulator: check-in of lost/lo item is within the ".
3810 "max return interval (or no interval is defined). Proceeding ".
3811 "with fine/fee voiding, etc.");
3813 $self->needs_lost_bill_handling(1);
3816 if ($circ_lib != $self->circ_lib) {
3817 # if the item is not home, check to see if we want to retain the
3818 # lost/longoverdue status at this point in the process
3820 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib,
3821 $args{ous_immediately_available}, $self->editor) || 0;
3823 if ($immediately_available) {
3824 # item status does not need to be retained, so give it a
3825 # reshelving status as if it were a normal checkin
3826 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3827 $self->copy->status($U->copy_status($next_status));
3830 $logger->info("circulator: leaving lost/longoverdue copy".
3831 " status in place on checkin");
3834 # lost/longoverdue item is home and processed, treat like a normal
3835 # checkin from this point on
3836 my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
3837 $self->copy->status($U->copy_status($next_status));
3843 sub checkin_handle_backdate {
3846 # ------------------------------------------------------------------
3847 # clean up the backdate for date comparison
3848 # XXX We are currently taking the due-time from the original due-date,
3849 # not the input. Do we need to do this? This certainly interferes with
3850 # backdating of hourly checkouts, but that is likely a very rare case.
3851 # ------------------------------------------------------------------
3852 my $bd = cleanse_ISO8601($self->backdate);
3853 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3854 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3855 $new_date->set_hour($original_date->hour());
3856 $new_date->set_minute($original_date->minute());
3857 if ($new_date >= DateTime->now) {
3858 # We can't say that the item will be checked in later...so assume someone's clock is wrong instead.
3859 # $self->backdate() autoload handler ignores undef values.
3860 # Clear the backdate manually.
3861 $logger->info("circulator: ignoring future backdate: $new_date");
3862 delete $self->{backdate};
3864 $self->backdate(cleanse_ISO8601($new_date->datetime()));
3871 sub check_checkin_copy_status {
3873 my $copy = $self->copy;
3875 my $status = $U->copy_status($copy->status)->id;
3878 if( $self->new_copy_alerts ||
3879 $status == OILS_COPY_STATUS_AVAILABLE ||
3880 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3881 $status == OILS_COPY_STATUS_IN_PROCESS ||
3882 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3883 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3884 $status == OILS_COPY_STATUS_CATALOGING ||
3885 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3886 $status == OILS_COPY_STATUS_CANCELED_TRANSIT ||
3887 $status == OILS_COPY_STATUS_RESHELVING );
3889 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3890 if( $status == OILS_COPY_STATUS_LOST );
3892 return OpenILS::Event->new('COPY_STATUS_LOST_AND_PAID', payload => $copy)
3893 if ($status == OILS_COPY_STATUS_LOST_AND_PAID);
3895 return OpenILS::Event->new('COPY_STATUS_LONG_OVERDUE', payload => $copy )
3896 if( $status == OILS_COPY_STATUS_LONG_OVERDUE );
3898 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3899 if( $status == OILS_COPY_STATUS_MISSING );
3901 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3906 # --------------------------------------------------------------------------
3907 # On checkin, we need to return as many relevant objects as we can
3908 # --------------------------------------------------------------------------
3909 sub checkin_flesh_events {
3912 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3913 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3914 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3917 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3920 if($self->hold and !$self->hold->cancel_time) {
3921 $hold = $self->hold;
3922 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3926 # update our copy of the circ object and
3927 # flesh the billing summary data
3929 $self->editor->retrieve_action_circulation([
3933 circ => ['billable_transaction'],
3942 # flesh some patron fields before returning
3944 $self->editor->retrieve_actor_user([
3949 au => ['card', 'billing_address', 'mailing_address']
3956 for my $evt (@{$self->events}) {
3959 $payload->{copy} = $U->unflesh_copy($self->copy);
3960 $payload->{volume} = $self->volume;
3961 $payload->{record} = $record,
3962 $payload->{circ} = $self->circ;
3963 $payload->{transit} = $self->transit;
3964 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3965 $payload->{hold} = $hold;
3966 $payload->{patron} = $self->patron;
3967 $payload->{reservation} = $self->reservation
3968 unless (not $self->reservation or $self->reservation->cancel_time);
3970 $evt->{payload} = $payload;
3975 my( $self, $msg ) = @_;
3976 my $bc = ($self->copy) ? $self->copy->barcode :
3979 my $usr = ($self->patron) ? $self->patron->id : "";
3980 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3981 ", recipient=$usr, copy=$bc");
3988 $self->log_me("do_renew()");
3990 # Make sure there is an open circ to renew
3991 my $usrid = $self->patron->id if $self->patron;
3992 my $circ = $self->editor->search_action_circulation({
3993 target_copy => $self->copy->id,
3994 xact_finish => undef,
3995 checkin_time => undef,
3996 ($usrid ? (usr => $usrid) : ())
3999 return $self->bail_on_events($self->editor->event) unless $circ;
4001 # A user is not allowed to renew another user's items without permission
4002 unless( $circ->usr eq $self->editor->requestor->id ) {
4003 return $self->bail_on_events($self->editor->events)
4004 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
4007 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
4008 if $circ->renewal_remaining < 1;
4010 $self->push_events(OpenILS::Event->new('MAX_AUTO_RENEWALS_REACHED'))
4011 if $api =~ /renew.auto/ and $circ->auto_renewal_remaining < 1;
4012 # -----------------------------------------------------------------
4014 $self->parent_circ($circ->id);
4015 $self->renewal_remaining( $circ->renewal_remaining - 1 );
4016 $self->auto_renewal_remaining( $circ->auto_renewal_remaining - 1 ) if (defined($circ->auto_renewal_remaining));
4019 # Opac renewal - re-use circ library from original circ (unless told not to)
4020 if($self->opac_renewal or $api =~ /renew.auto/) {
4021 unless(defined($opac_renewal_use_circ_lib)) {
4022 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
4023 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
4024 $opac_renewal_use_circ_lib = 1;
4027 $opac_renewal_use_circ_lib = 0;
4030 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
4033 # Desk renewal - re-use circ library from original circ (unless told not to)
4034 if($self->desk_renewal) {
4035 unless(defined($desk_renewal_use_circ_lib)) {
4036 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.desk_renewal.use_original_circ_lib');
4037 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
4038 $desk_renewal_use_circ_lib = 1;
4041 $desk_renewal_use_circ_lib = 0;
4044 $self->circ_lib($circ->circ_lib) if($desk_renewal_use_circ_lib);
4047 # Run the fine generator against the old circ
4048 # XXX This seems unnecessary, given that handle_fines runs in do_checkin
4049 # a few lines down. Commenting out, for now.
4050 #$self->handle_fines;
4052 $self->run_renew_permit;
4055 $self->do_checkin();
4056 return if $self->bail_out;
4058 unless( $self->permit_override ) {
4060 return if $self->bail_out;
4061 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
4062 $self->remove_event('ITEM_NOT_CATALOGED');
4065 $self->override_events;
4066 return if $self->bail_out;
4069 $self->do_checkout();
4074 my( $self, $evt ) = @_;
4075 $evt = (ref $evt) ? $evt->{textcode} : $evt;
4076 $logger->debug("circulator: removing event from list: $evt");
4077 my @events = @{$self->events};
4078 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
4083 my( $self, $evt ) = @_;
4084 $evt = (ref $evt) ? $evt->{textcode} : $evt;
4085 return grep { $_->{textcode} eq $evt } @{$self->events};
4089 sub run_renew_permit {
4092 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
4093 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
4094 $self->editor, $self->copy, $self->editor->requestor, 1
4096 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
4099 my $results = $self->run_indb_circ_test;
4100 $self->push_events($self->matrix_test_result_events)
4101 unless $self->circ_test_success;
4105 # XXX: The primary mechanism for storing circ history is now handled
4106 # by tracking real circulation objects instead of bibs in a bucket.
4107 # However, this code is disabled by default and could be useful
4108 # some day, so may as well leave it for now.
4109 sub append_reading_list {
4113 $self->is_checkout and
4119 # verify history is globally enabled and uses the bucket mechanism
4120 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
4121 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
4123 return undef unless $htype and $htype eq 'bucket';
4125 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
4127 # verify the patron wants to retain the hisory
4128 my $setting = $e->search_actor_user_setting(
4129 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
4131 unless($setting and $setting->value) {
4136 my $bkt = $e->search_container_copy_bucket(
4137 {owner => $self->patron->id, btype => 'circ_history'})->[0];
4142 # find the next item position
4143 my $last_item = $e->search_container_copy_bucket_item(
4144 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
4145 $pos = $last_item->pos + 1 if $last_item;
4148 # create the history bucket if necessary
4149 $bkt = Fieldmapper::container::copy_bucket->new;
4150 $bkt->owner($self->patron->id);
4152 $bkt->btype('circ_history');
4154 $e->create_container_copy_bucket($bkt) or return $e->die_event;
4157 my $item = Fieldmapper::container::copy_bucket_item->new;
4159 $item->bucket($bkt->id);
4160 $item->target_copy($self->copy->id);
4163 $e->create_container_copy_bucket_item($item) or return $e->die_event;
4170 sub make_trigger_events {
4172 return unless $self->circ;
4173 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
4174 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
4175 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
4180 sub checkin_handle_lost_or_lo_now_found {
4181 my ($self, $bill_type, $is_longoverdue) = @_;
4183 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4185 $logger->debug("voiding $tag item billings");
4186 my $result = $CC->void_or_zero_bills_of_type($self->editor, $self->circ, $self->copy, $bill_type, "$tag ITEM RETURNED");
4187 $self->bail_on_events($self->editor->event) if ($result);
4190 sub checkin_handle_lost_or_lo_now_found_restore_od {
4192 my $circ_lib = shift;
4193 my $is_longoverdue = shift;
4194 my $tag = $is_longoverdue ? "LONGOVERDUE" : "LOST";
4196 # ------------------------------------------------------------------
4197 # restore those overdue charges voided when item was set to lost
4198 # ------------------------------------------------------------------
4200 my $ods = $self->editor->search_money_billing([
4202 xact => $self->circ->id,
4206 order_by => {mb => 'billing_ts desc'}
4210 $logger->debug("returning ".scalar(@$ods)." overdue charges pre-$tag");
4211 # Because actual users get up to all kinds of unexpectedness, we
4212 # only recreate up to $circ->max_fine in bills. I know you think
4213 # it wouldn't happen that bills could get created, voided, and
4214 # recreated more than once, but I guaran-damn-tee you that it will
4216 if ($ods && @$ods) {
4217 my $void_amount = 0;
4218 my $void_max = $self->circ->max_fine();
4219 # search for overdues voided the new way (aka "adjusted")
4220 my @billings = map {$_->id()} @$ods;
4221 my $voids = $self->editor->search_money_account_adjustment(
4223 billing => \@billings
4227 map {$void_amount += $_->amount()} @$voids;
4229 # if no adjustments found, assume they were voided the old way (aka "voided")
4230 for my $bill (@$ods) {
4231 if( $U->is_true($bill->voided) ) {
4232 $void_amount += $bill->amount();
4238 ($void_amount < $void_max ? $void_amount : $void_max),
4240 $ods->[0]->billing_type(),
4242 "System: $tag RETURNED - OVERDUES REINSTATED",
4243 $ods->[-1]->period_start(),
4244 $ods->[0]->period_end() # date this restoration the same as the last overdue (for possible subsequent fine generation)