1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
11 my $U = "OpenILS::Application::AppUtils";
15 my $legacy_script_support = 0;
17 my $opac_renewal_use_circ_lib;
19 sub determine_booking_status {
20 unless (defined $booking_status) {
21 my $ses = create OpenSRF::AppSession("router");
22 $booking_status = grep {$_ eq "open-ils.booking"} @{
23 $ses->request("opensrf.router.info.class.list")->gather(1)
26 $logger->info("booking status: " . ($booking_status ? "on" : "off"));
29 return $booking_status;
35 flesh_fields => {acp => ['call_number','parts'], acn => ['record']}
41 my $conf = OpenSRF::Utils::SettingsClient->new;
42 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
44 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
45 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
47 my $lb = $conf->config_value( @pfx2, 'script_path' );
48 $lb = [ $lb ] unless ref($lb);
51 return unless $legacy_script_support;
53 my @pfx = ( @pfx2, "scripts" );
54 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
55 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
56 my $d = $conf->config_value( @pfx, 'circ_duration' );
57 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
58 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
59 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
61 $logger->error( "Missing circ script(s)" )
62 unless( $p and $c and $d and $f and $m and $pr );
64 $scripts{circ_permit_patron} = $p;
65 $scripts{circ_permit_copy} = $c;
66 $scripts{circ_duration} = $d;
67 $scripts{circ_recurring_fines} = $f;
68 $scripts{circ_max_fines} = $m;
69 $scripts{circ_permit_renew} = $pr;
72 "circulator: Loaded rules scripts for circ: " .
73 "circ permit patron = $p, ".
74 "circ permit copy = $c, ".
75 "circ duration = $d, ".
76 "circ recurring fines = $f, " .
77 "circ max fines = $m, ".
78 "circ renew permit = $pr. ".
80 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
84 __PACKAGE__->register_method(
85 method => "run_method",
86 api_name => "open-ils.circ.checkout.permit",
88 Determines if the given checkout can occur
89 @param authtoken The login session key
90 @param params A trailing hash of named params including
91 barcode : The copy barcode,
92 patron : The patron the checkout is occurring for,
93 renew : true or false - whether or not this is a renewal
94 @return The event that occurred during the permit check.
98 __PACKAGE__->register_method (
99 method => 'run_method',
100 api_name => 'open-ils.circ.checkout.permit.override',
101 signature => q/@see open-ils.circ.checkout.permit/,
105 __PACKAGE__->register_method(
106 method => "run_method",
107 api_name => "open-ils.circ.checkout",
110 @param authtoken The login session key
111 @param params A named hash of params including:
113 barcode If no copy is provided, the copy is retrieved via barcode
114 copyid If no copy or barcode is provide, the copy id will be use
115 patron The patron's id
116 noncat True if this is a circulation for a non-cataloted item
117 noncat_type The non-cataloged type id
118 noncat_circ_lib The location for the noncat circ.
119 precat The item has yet to be cataloged
120 dummy_title The temporary title of the pre-cataloded item
121 dummy_author The temporary authr of the pre-cataloded item
122 Default is the home org of the staff member
123 @return The SUCCESS event on success, any other event depending on the error
126 __PACKAGE__->register_method(
127 method => "run_method",
128 api_name => "open-ils.circ.checkin",
131 Generic super-method for handling all copies
132 @param authtoken The login session key
133 @param params Hash of named parameters including:
134 barcode - The copy barcode
135 force - If true, copies in bad statuses will be checked in and give good statuses
136 noop - don't capture holds or put items into transit
137 void_overdues - void all overdues for the circulation (aka amnesty)
142 __PACKAGE__->register_method(
143 method => "run_method",
144 api_name => "open-ils.circ.checkin.override",
145 signature => q/@see open-ils.circ.checkin/
148 __PACKAGE__->register_method(
149 method => "run_method",
150 api_name => "open-ils.circ.renew.override",
151 signature => q/@see open-ils.circ.renew/,
155 __PACKAGE__->register_method(
156 method => "run_method",
157 api_name => "open-ils.circ.renew",
158 notes => <<" NOTES");
159 PARAMS( authtoken, circ => circ_id );
160 open-ils.circ.renew(login_session, circ_object);
161 Renews the provided circulation. login_session is the requestor of the
162 renewal and if the logged in user is not the same as circ->usr, then
163 the logged in user must have RENEW_CIRC permissions.
166 __PACKAGE__->register_method(
167 method => "run_method",
168 api_name => "open-ils.circ.checkout.full"
170 __PACKAGE__->register_method(
171 method => "run_method",
172 api_name => "open-ils.circ.checkout.full.override"
174 __PACKAGE__->register_method(
175 method => "run_method",
176 api_name => "open-ils.circ.reservation.pickup"
178 __PACKAGE__->register_method(
179 method => "run_method",
180 api_name => "open-ils.circ.reservation.return"
182 __PACKAGE__->register_method(
183 method => "run_method",
184 api_name => "open-ils.circ.reservation.return.override"
186 __PACKAGE__->register_method(
187 method => "run_method",
188 api_name => "open-ils.circ.checkout.inspect",
189 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
194 my( $self, $conn, $auth, $args ) = @_;
195 translate_legacy_args($args);
196 $args->{override_args} = { all => 1 } unless defined $args->{override_args};
197 my $api = $self->api_name;
200 OpenILS::Application::Circ::Circulator->new($auth, %$args);
202 return circ_events($circulator) if $circulator->bail_out;
204 $circulator->use_booking(determine_booking_status());
206 # --------------------------------------------------------------------------
207 # First, check for a booking transit, as the barcode may not be a copy
208 # barcode, but a resource barcode, and nothing else in here will work
209 # --------------------------------------------------------------------------
211 if ($circulator->use_booking && (my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
212 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
213 if (@$resources) { # yes!
215 my $res_id_list = [ map { $_->id } @$resources ];
216 my $transit = $circulator->editor->search_action_reservation_transit_copy(
218 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
219 { order_by => { artc => 'source_send_time' }, limit => 1 }
221 )->[0]; # Any transit for this barcode?
223 if ($transit) { # yes! unwrap it.
225 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
226 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
228 my $success_event = new OpenILS::Event(
229 "SUCCESS", "payload" => {"reservation" => $reservation}
231 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
232 if (my $copy = $circulator->editor->search_asset_copy([
233 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
234 ])->[0]) { # got a copy
235 $copy->status( $transit->copy_status );
236 $copy->editor($circulator->editor->requestor->id);
237 $copy->edit_date('now');
238 $circulator->editor->update_asset_copy($copy);
239 $success_event->{"payload"}->{"record"} =
240 $U->record_to_mvr($copy->call_number->record);
241 $success_event->{"payload"}->{"volume"} = $copy->call_number;
242 $copy->call_number($copy->call_number->id);
243 $success_event->{"payload"}->{"copy"} = $copy;
247 $transit->dest_recv_time('now');
248 $circulator->editor->update_action_reservation_transit_copy( $transit );
250 $circulator->editor->commit;
251 # Formerly this branch just stopped here. Argh!
252 $conn->respond_complete($success_event);
260 # --------------------------------------------------------------------------
261 # Go ahead and load the script runner to make sure we have all
262 # of the objects we need
263 # --------------------------------------------------------------------------
265 if ($circulator->use_booking) {
266 $circulator->is_res_checkin($circulator->is_checkin(1))
267 if $api =~ /reservation.return/ or (
268 $api =~ /checkin/ and $circulator->seems_like_reservation()
271 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
274 $circulator->is_renewal(1) if $api =~ /renew/;
275 $circulator->is_checkin(1) if $api =~ /checkin/;
277 $circulator->mk_env();
278 $circulator->noop(1) if $circulator->claims_never_checked_out;
280 if($legacy_script_support and not $circulator->is_checkin) {
281 $circulator->mk_script_runner();
282 $circulator->legacy_script_support(1);
283 $circulator->circ_permit_patron($scripts{circ_permit_patron});
284 $circulator->circ_permit_copy($scripts{circ_permit_copy});
285 $circulator->circ_duration($scripts{circ_duration});
286 $circulator->circ_permit_renew($scripts{circ_permit_renew});
288 return circ_events($circulator) if $circulator->bail_out;
291 $circulator->override(1) if $api =~ /override/o;
293 if( $api =~ /checkout\.permit/ ) {
294 $circulator->do_permit();
296 } elsif( $api =~ /checkout.full/ ) {
298 # requesting a precat checkout implies that any required
299 # overrides have been performed. Go ahead and re-override.
300 $circulator->skip_permit_key(1);
301 $circulator->override(1) if $circulator->request_precat;
302 $circulator->do_permit();
303 $circulator->is_checkout(1);
304 unless( $circulator->bail_out ) {
305 $circulator->events([]);
306 $circulator->do_checkout();
309 } elsif( $circulator->is_res_checkout ) {
310 $circulator->do_reservation_pickup();
312 } elsif( $api =~ /inspect/ ) {
313 my $data = $circulator->do_inspect();
314 $circulator->editor->rollback;
317 } elsif( $api =~ /checkout/ ) {
318 $circulator->is_checkout(1);
319 $circulator->do_checkout();
321 } elsif( $circulator->is_res_checkin ) {
322 $circulator->do_reservation_return();
323 $circulator->do_checkin() if ($circulator->copy());
324 } elsif( $api =~ /checkin/ ) {
325 $circulator->do_checkin();
327 } elsif( $api =~ /renew/ ) {
328 $circulator->is_renewal(1);
329 $circulator->do_renew();
332 if( $circulator->bail_out ) {
335 # make sure no success event accidentally slip in
337 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
340 my @e = @{$circulator->events};
341 push( @ee, $_->{textcode} ) for @e;
342 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
344 $circulator->editor->rollback;
348 $circulator->editor->commit;
350 if ($circulator->generate_lost_overdue) {
351 # Generating additional overdue billings has to happen after the
352 # main commit and before the final respond() so the caller can
353 # receive the latest transaction summary.
354 my $evt = $circulator->generate_lost_overdue_fines;
355 $circulator->bail_on_events($evt) if $evt;
359 $conn->respond_complete(circ_events($circulator));
361 $circulator->script_runner->cleanup if $circulator->script_runner;
363 return undef if $circulator->bail_out;
365 $circulator->do_hold_notify($circulator->notify_hold)
366 if $circulator->notify_hold;
367 $circulator->retarget_holds if $circulator->retarget;
368 $circulator->append_reading_list;
369 $circulator->make_trigger_events;
376 my @e = @{$circ->events};
377 # if we have multiple events, SUCCESS should not be one of them;
378 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
379 return (@e == 1) ? $e[0] : \@e;
383 sub translate_legacy_args {
386 if( $$args{barcode} ) {
387 $$args{copy_barcode} = $$args{barcode};
388 delete $$args{barcode};
391 if( $$args{copyid} ) {
392 $$args{copy_id} = $$args{copyid};
393 delete $$args{copyid};
396 if( $$args{patronid} ) {
397 $$args{patron_id} = $$args{patronid};
398 delete $$args{patronid};
401 if( $$args{patron} and !ref($$args{patron}) ) {
402 $$args{patron_id} = $$args{patron};
403 delete $$args{patron};
407 if( $$args{noncat} ) {
408 $$args{is_noncat} = $$args{noncat};
409 delete $$args{noncat};
412 if( $$args{precat} ) {
413 $$args{is_precat} = $$args{request_precat} = $$args{precat};
414 delete $$args{precat};
420 # --------------------------------------------------------------------------
421 # This package actually manages all of the circulation logic
422 # --------------------------------------------------------------------------
423 package OpenILS::Application::Circ::Circulator;
424 use strict; use warnings;
425 use vars q/$AUTOLOAD/;
427 use OpenILS::Utils::Fieldmapper;
428 use OpenSRF::Utils::Cache;
429 use Digest::MD5 qw(md5_hex);
430 use DateTime::Format::ISO8601;
431 use OpenILS::Utils::PermitHold;
432 use OpenSRF::Utils qw/:datetime/;
433 use OpenSRF::Utils::SettingsClient;
434 use OpenILS::Application::Circ::Holds;
435 use OpenILS::Application::Circ::Transit;
436 use OpenSRF::Utils::Logger qw(:logger);
437 use OpenILS::Utils::CStoreEditor qw/:funcs/;
438 use OpenILS::Application::Circ::ScriptBuilder;
439 use OpenILS::Const qw/:const/;
440 use OpenILS::Utils::Penalty;
441 use OpenILS::Application::Circ::CircCommon;
444 my $holdcode = "OpenILS::Application::Circ::Holds";
445 my $transcode = "OpenILS::Application::Circ::Transit";
451 # --------------------------------------------------------------------------
452 # Add a pile of automagic getter/setter methods
453 # --------------------------------------------------------------------------
454 my @AUTOLOAD_FIELDS = qw/
501 recurring_fines_level
514 cancelled_hold_transit
521 circ_matrix_matchpoint
523 legacy_script_support
533 claims_never_checked_out
538 generate_lost_overdue
551 my $type = ref($self) or die "$self is not an object";
553 my $name = $AUTOLOAD;
556 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
557 $logger->error("circulator: $type: invalid autoload field: $name");
558 die "$type: invalid autoload field: $name\n"
563 *{"${type}::${name}"} = sub {
566 $s->{$name} = $v if defined $v;
570 return $self->$name($data);
575 my( $class, $auth, %args ) = @_;
576 $class = ref($class) || $class;
577 my $self = bless( {}, $class );
580 $self->editor(new_editor(xact => 1, authtoken => $auth));
582 unless( $self->editor->checkauth ) {
583 $self->bail_on_events($self->editor->event);
587 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
589 $self->$_($args{$_}) for keys %args;
592 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
594 # if this is a renewal, default to desk_renewal
595 $self->desk_renewal(1) unless
596 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
598 $self->capture('') unless $self->capture;
600 unless(%user_groups) {
601 my $gps = $self->editor->retrieve_all_permission_grp_tree;
602 %user_groups = map { $_->id => $_ } @$gps;
609 # --------------------------------------------------------------------------
610 # True if we should discontinue processing
611 # --------------------------------------------------------------------------
613 my( $self, $bool ) = @_;
614 if( defined $bool ) {
615 $logger->info("circulator: BAILING OUT") if $bool;
616 $self->{bail_out} = $bool;
618 return $self->{bail_out};
623 my( $self, @evts ) = @_;
626 $e->{payload} = $self->copy if
627 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
629 $logger->info("circulator: pushing event ".$e->{textcode});
630 push( @{$self->events}, $e ) unless
631 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
637 return '' if $self->skip_permit_key;
638 my $key = md5_hex( time() . rand() . "$$" );
639 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
640 return $self->permit_key($key);
643 sub check_permit_key {
645 return 1 if $self->skip_permit_key;
646 my $key = $self->permit_key;
647 return 0 unless $key;
648 my $k = "oils_permit_key_$key";
649 my $one = $self->cache_handle->get_cache($k);
650 $self->cache_handle->delete_cache($k);
651 return ($one) ? 1 : 0;
654 sub seems_like_reservation {
657 # Some words about the following method:
658 # 1) It requires the VIEW_USER permission, but that's not an
659 # issue, right, since all staff should have that?
660 # 2) It returns only one reservation at a time, even if an item can be
661 # and is currently overbooked. Hmmm....
662 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
663 my $result = $booking_ses->request(
664 "open-ils.booking.reservations.by_returnable_resource_barcode",
665 $self->editor->authtoken,
668 $booking_ses->disconnect;
670 return $self->bail_on_events($result) if defined $U->event_code($result);
673 $self->reservation(shift @$result);
681 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
682 sub save_trimmed_copy {
683 my ($self, $copy) = @_;
686 $self->volume($copy->call_number);
687 $self->title($self->volume->record);
688 $self->copy->call_number($self->volume->id);
689 $self->volume->record($self->title->id);
690 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
691 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
692 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
693 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
699 my $e = $self->editor;
701 # --------------------------------------------------------------------------
702 # Grab the fleshed copy
703 # --------------------------------------------------------------------------
704 unless($self->is_noncat) {
707 $copy = $e->retrieve_asset_copy(
708 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
710 } elsif( $self->copy_barcode ) {
712 $copy = $e->search_asset_copy(
713 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
714 } elsif( $self->reservation ) {
715 my $res = $e->json_query(
717 "select" => {"acp" => ["id"]},
722 "field" => "barcode",
726 "field" => "current_resource"
734 "id" => (ref $self->reservation) ?
735 $self->reservation->id : $self->reservation
740 if (ref $res eq "ARRAY" and scalar @$res) {
741 $logger->info("circulator: mapped reservation " .
742 $self->reservation . " to copy " . $res->[0]->{"id"});
743 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
748 $self->save_trimmed_copy($copy);
750 # We can't renew if there is no copy
751 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
752 if $self->is_renewal;
757 # --------------------------------------------------------------------------
759 # --------------------------------------------------------------------------
763 flesh_fields => {au => [ qw/ card / ]}
766 if( $self->patron_id ) {
767 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
768 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
770 } elsif( $self->patron_barcode ) {
772 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
773 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
774 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
776 $patron = $e->retrieve_actor_user($card->usr)
777 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
779 # Use the card we looked up, not the patron's primary, for card active checks
780 $patron->card($card);
783 if( my $copy = $self->copy ) {
786 $flesh->{flesh_fields}->{circ} = ['usr'];
788 my $circ = $e->search_action_circulation([
789 {target_copy => $copy->id, checkin_time => undef}, $flesh
793 $patron = $circ->usr;
794 $circ->usr($patron->id); # de-flesh for consistency
800 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
801 unless $self->patron($patron) or $self->is_checkin;
803 unless($self->is_checkin) {
805 # Check for inactivity and patron reg. expiration
807 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
808 unless $U->is_true($patron->active);
810 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
811 unless $U->is_true($patron->card->active);
813 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
814 cleanse_ISO8601($patron->expire_date));
816 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
817 if( CORE::time > $expire->epoch ) ;
821 # --------------------------------------------------------------------------
822 # This builds the script runner environment and fetches most of the
824 # --------------------------------------------------------------------------
825 sub mk_script_runner {
831 qw/copy copy_barcode copy_id patron
832 patron_id patron_barcode volume title editor/;
834 # Translate our objects into the ScriptBuilder args hash
835 $$args{$_} = $self->$_() for @fields;
837 $args->{ignore_user_status} = 1 if $self->is_checkin;
838 $$args{fetch_patron_by_circ_copy} = 1;
839 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
841 if( my $pco = $self->pending_checkouts ) {
842 $logger->info("circulator: we were given a pending checkouts number of $pco");
843 $$args{patronItemsOut} = $pco;
846 # This fetches most of the objects we need
847 $self->script_runner(
848 OpenILS::Application::Circ::ScriptBuilder->build($args));
850 # Now we translate the ScriptBuilder objects back into self
851 $self->$_($$args{$_}) for @fields;
853 my @evts = @{$args->{_events}} if $args->{_events};
855 $logger->debug("circulator: script builder returned events: @evts") if @evts;
859 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
860 if(!$self->is_noncat and
862 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
866 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
867 return $self->bail_on_events(@e);
872 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
873 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
874 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
875 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
879 # We can't renew if there is no copy
880 return $self->bail_on_events(@evts) if
881 $self->is_renewal and !$self->copy;
883 # Set some circ-specific flags in the script environment
884 my $evt = "environment";
885 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
887 if( $self->is_noncat ) {
888 $self->script_runner->insert("$evt.isNonCat", 1);
889 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
892 if( $self->is_precat ) {
893 $self->script_runner->insert("environment.isPrecat", 1, 1);
896 $self->script_runner->add_path( $_ ) for @$script_libs;
901 # --------------------------------------------------------------------------
902 # Does the circ permit work
903 # --------------------------------------------------------------------------
907 $self->log_me("do_permit()");
909 unless( $self->editor->requestor->id == $self->patron->id ) {
910 return $self->bail_on_events($self->editor->event)
911 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
914 $self->check_captured_holds();
915 $self->do_copy_checks();
916 return if $self->bail_out;
917 $self->run_patron_permit_scripts();
918 $self->run_copy_permit_scripts()
919 unless $self->is_precat or $self->is_noncat;
920 $self->check_item_deposit_events();
921 $self->override_events();
922 return if $self->bail_out;
924 if($self->is_precat and not $self->request_precat) {
927 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
928 return $self->bail_out(1) unless $self->is_renewal;
932 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
935 sub check_item_deposit_events {
937 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
938 if $self->is_deposit and not $self->is_deposit_exempt;
939 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
940 if $self->is_rental and not $self->is_rental_exempt;
943 # returns true if the user is not required to pay deposits
944 sub is_deposit_exempt {
946 my $pid = (ref $self->patron->profile) ?
947 $self->patron->profile->id : $self->patron->profile;
948 my $groups = $U->ou_ancestor_setting_value(
949 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
950 for my $grp (@$groups) {
951 return 1 if $self->is_group_descendant($grp, $pid);
956 # returns true if the user is not required to pay rental fees
957 sub is_rental_exempt {
959 my $pid = (ref $self->patron->profile) ?
960 $self->patron->profile->id : $self->patron->profile;
961 my $groups = $U->ou_ancestor_setting_value(
962 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
963 for my $grp (@$groups) {
964 return 1 if $self->is_group_descendant($grp, $pid);
969 sub is_group_descendant {
970 my($self, $p_id, $c_id) = @_;
971 return 0 unless defined $p_id and defined $c_id;
972 return 1 if $c_id == $p_id;
973 while(my $grp = $user_groups{$c_id}) {
974 $c_id = $grp->parent;
975 return 0 unless defined $c_id;
976 return 1 if $c_id == $p_id;
981 sub check_captured_holds {
983 my $copy = $self->copy;
984 my $patron = $self->patron;
986 return undef unless $copy;
988 my $s = $U->copy_status($copy->status)->id;
989 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
990 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
992 # Item is on the holds shelf, make sure it's going to the right person
993 my $hold = $self->editor->search_action_hold_request(
996 current_copy => $copy->id ,
997 capture_time => { '!=' => undef },
998 cancel_time => undef,
999 fulfillment_time => undef
1005 if ($hold and $hold->usr == $patron->id) {
1006 $self->checkout_is_for_hold(1);
1010 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
1012 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
1016 sub do_copy_checks {
1018 my $copy = $self->copy;
1019 return unless $copy;
1021 my $stat = $U->copy_status($copy->status)->id;
1023 # We cannot check out a copy if it is in-transit
1024 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
1025 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
1028 $self->handle_claims_returned();
1029 return if $self->bail_out;
1031 # no claims returned circ was found, check if there is any open circ
1032 unless( $self->is_renewal ) {
1034 my $circs = $self->editor->search_action_circulation(
1035 { target_copy => $copy->id, checkin_time => undef }
1038 if(my $old_circ = $circs->[0]) { # an open circ was found
1040 my $payload = {copy => $copy};
1042 if($old_circ->usr == $self->patron->id) {
1044 $payload->{old_circ} = $old_circ;
1046 # If there is an open circulation on the checkout item and an auto-renew
1047 # interval is defined, inform the caller that they should go
1048 # ahead and renew the item instead of warning about open circulations.
1050 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1052 'circ.checkout_auto_renew_age',
1056 if($auto_renew_intvl) {
1057 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1058 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1060 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1061 $payload->{auto_renew} = 1;
1066 return $self->bail_on_events(
1067 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1073 my $LEGACY_CIRC_EVENT_MAP = {
1074 'no_item' => 'ITEM_NOT_CATALOGED',
1075 'actor.usr.barred' => 'PATRON_BARRED',
1076 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1077 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1078 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1079 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1080 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1081 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1082 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1083 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1087 # ---------------------------------------------------------------------
1088 # This pushes any patron-related events into the list but does not
1089 # set bail_out for any events
1090 # ---------------------------------------------------------------------
1091 sub run_patron_permit_scripts {
1093 my $runner = $self->script_runner;
1094 my $patronid = $self->patron->id;
1098 if(!$self->legacy_script_support) {
1100 my $results = $self->run_indb_circ_test;
1101 unless($self->circ_test_success) {
1102 my @trimmed_results;
1104 if ($self->is_noncat) {
1105 # no_item result is OK during noncat checkout
1106 @trimmed_results = grep { ($_->{fail_part} || '') ne 'no_item' } @$results;
1109 if ($self->checkout_is_for_hold) {
1110 # if this checkout will fulfill a hold, ignore CIRC blocks
1111 # and rely instead on the (later-checked) FULFILL block
1113 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1114 my $fblock_pens = $self->editor->search_config_standing_penalty(
1115 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1117 for my $res (@$results) {
1118 my $name = $res->{fail_part} || '';
1119 next if grep {$_->name eq $name} @$fblock_pens or
1120 ($self->is_noncat and $name eq 'no_item');
1121 push(@trimmed_results, $res);
1125 # update the final set of test results
1126 $self->matrix_test_result(\@trimmed_results);
1128 push @allevents, $self->matrix_test_result_events;
1133 # ---------------------------------------------------------------------
1134 # # Now run the patron permit script
1135 # ---------------------------------------------------------------------
1136 $runner->load($self->circ_permit_patron);
1137 my $result = $runner->run or
1138 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1140 my $patron_events = $result->{events};
1142 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1143 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1144 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1145 $penalties = $penalties->{fatal_penalties};
1147 for my $pen (@$penalties) {
1148 # CIRC blocks are ignored if this is a FULFILL scenario
1149 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1150 my $event = OpenILS::Event->new($pen->name);
1151 $event->{desc} = $pen->label;
1152 push(@allevents, $event);
1155 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1159 $_->{payload} = $self->copy if
1160 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1163 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1165 $self->push_events(@allevents);
1168 sub matrix_test_result_codes {
1170 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1173 sub matrix_test_result_events {
1176 my $event = new OpenILS::Event(
1177 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1179 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1181 } (@{$self->matrix_test_result});
1184 sub run_indb_circ_test {
1186 return $self->matrix_test_result if $self->matrix_test_result;
1188 my $dbfunc = ($self->is_renewal) ?
1189 'action.item_user_renew_test' : 'action.item_user_circ_test';
1191 if( $self->is_precat && $self->request_precat) {
1192 $self->make_precat_copy;
1193 return if $self->bail_out;
1196 my $results = $self->editor->json_query(
1200 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1206 $self->circ_test_success($U->is_true($results->[0]->{success}));
1208 if(my $mp = $results->[0]->{matchpoint}) {
1209 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1210 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1211 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1212 if(defined($results->[0]->{renewals})) {
1213 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1215 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1216 if(defined($results->[0]->{grace_period})) {
1217 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1219 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1220 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1221 # Grab the *last* response for limit_groups, where it is more likely to be filled
1222 $self->limit_groups($results->[-1]->{limit_groups});
1225 return $self->matrix_test_result($results);
1228 # ---------------------------------------------------------------------
1229 # given a use and copy, this will calculate the circulation policy
1230 # parameters. Only works with in-db circ.
1231 # ---------------------------------------------------------------------
1235 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1237 $self->run_indb_circ_test;
1240 circ_test_success => $self->circ_test_success,
1241 failure_events => [],
1242 failure_codes => [],
1243 matchpoint => $self->circ_matrix_matchpoint
1246 unless($self->circ_test_success) {
1247 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1248 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1251 if($self->circ_matrix_matchpoint) {
1252 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1253 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1254 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1255 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1257 my $policy = $self->get_circ_policy(
1258 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1260 $$results{$_} = $$policy{$_} for keys %$policy;
1266 # ---------------------------------------------------------------------
1267 # Loads the circ policy info for duration, recurring fine, and max
1268 # fine based on the current copy
1269 # ---------------------------------------------------------------------
1270 sub get_circ_policy {
1271 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1274 duration_rule => $duration_rule->name,
1275 recurring_fine_rule => $recurring_fine_rule->name,
1276 max_fine_rule => $max_fine_rule->name,
1277 max_fine => $self->get_max_fine_amount($max_fine_rule),
1278 fine_interval => $recurring_fine_rule->recurrence_interval,
1279 renewal_remaining => $duration_rule->max_renewals,
1280 grace_period => $recurring_fine_rule->grace_period
1283 if($hard_due_date) {
1284 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1285 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1288 $policy->{duration_date_ceiling} = undef;
1289 $policy->{duration_date_ceiling_force} = undef;
1292 $policy->{duration} = $duration_rule->shrt
1293 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1294 $policy->{duration} = $duration_rule->normal
1295 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1296 $policy->{duration} = $duration_rule->extended
1297 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1299 $policy->{recurring_fine} = $recurring_fine_rule->low
1300 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1301 $policy->{recurring_fine} = $recurring_fine_rule->normal
1302 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1303 $policy->{recurring_fine} = $recurring_fine_rule->high
1304 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1309 sub get_max_fine_amount {
1311 my $max_fine_rule = shift;
1312 my $max_amount = $max_fine_rule->amount;
1314 # if is_percent is true then the max->amount is
1315 # use as a percentage of the copy price
1316 if ($U->is_true($max_fine_rule->is_percent)) {
1317 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1318 $max_amount = $price * $max_fine_rule->amount / 100;
1320 $U->ou_ancestor_setting_value(
1322 'circ.max_fine.cap_at_price',
1326 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1327 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1335 sub run_copy_permit_scripts {
1337 my $copy = $self->copy || return;
1338 my $runner = $self->script_runner;
1342 if(!$self->legacy_script_support) {
1343 my $results = $self->run_indb_circ_test;
1344 push @allevents, $self->matrix_test_result_events
1345 unless $self->circ_test_success;
1348 # ---------------------------------------------------------------------
1349 # Capture all of the copy permit events
1350 # ---------------------------------------------------------------------
1351 $runner->load($self->circ_permit_copy);
1352 my $result = $runner->run or
1353 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1354 my $copy_events = $result->{events};
1356 # ---------------------------------------------------------------------
1357 # Now collect all of the events together
1358 # ---------------------------------------------------------------------
1359 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1362 # See if this copy has an alert message
1363 my $ae = $self->check_copy_alert();
1364 push( @allevents, $ae ) if $ae;
1366 # uniquify the events
1367 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1368 @allevents = values %hash;
1370 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1372 $self->push_events(@allevents);
1376 sub check_copy_alert {
1378 return undef if $self->is_renewal;
1379 return OpenILS::Event->new(
1380 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1381 if $self->copy and $self->copy->alert_message;
1387 # --------------------------------------------------------------------------
1388 # If the call is overriding and has permissions to override every collected
1389 # event, the are cleared. Any event that the caller does not have
1390 # permission to override, will be left in the event list and bail_out will
1392 # XXX We need code in here to cancel any holds/transits on copies
1393 # that are being force-checked out
1394 # --------------------------------------------------------------------------
1395 sub override_events {
1397 my @events = @{$self->events};
1398 return unless @events;
1399 my $oargs = $self->override_args;
1401 if(!$self->override) {
1402 return $self->bail_out(1)
1403 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1408 for my $e (@events) {
1409 my $tc = $e->{textcode};
1410 next if $tc eq 'SUCCESS';
1411 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1412 my $ov = "$tc.override";
1413 $logger->info("circulator: attempting to override event: $ov");
1415 return $self->bail_on_events($self->editor->event)
1416 unless( $self->editor->allowed($ov) );
1418 return $self->bail_out(1);
1424 # --------------------------------------------------------------------------
1425 # If there is an open claimsreturn circ on the requested copy, close the
1426 # circ if overriding, otherwise bail out
1427 # --------------------------------------------------------------------------
1428 sub handle_claims_returned {
1430 my $copy = $self->copy;
1432 my $CR = $self->editor->search_action_circulation(
1434 target_copy => $copy->id,
1435 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1436 checkin_time => undef,
1440 return unless ($CR = $CR->[0]);
1444 # - If the caller has set the override flag, we will check the item in
1445 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1447 $CR->checkin_time('now');
1448 $CR->checkin_scan_time('now');
1449 $CR->checkin_lib($self->circ_lib);
1450 $CR->checkin_workstation($self->editor->requestor->wsid);
1451 $CR->checkin_staff($self->editor->requestor->id);
1453 $evt = $self->editor->event
1454 unless $self->editor->update_action_circulation($CR);
1457 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1460 $self->bail_on_events($evt) if $evt;
1465 # --------------------------------------------------------------------------
1466 # This performs the checkout
1467 # --------------------------------------------------------------------------
1471 $self->log_me("do_checkout()");
1473 # make sure perms are good if this isn't a renewal
1474 unless( $self->is_renewal ) {
1475 return $self->bail_on_events($self->editor->event)
1476 unless( $self->editor->allowed('COPY_CHECKOUT') );
1479 # verify the permit key
1480 unless( $self->check_permit_key ) {
1481 if( $self->permit_override ) {
1482 return $self->bail_on_events($self->editor->event)
1483 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1485 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1489 # if this is a non-cataloged circ, build the circ and finish
1490 if( $self->is_noncat ) {
1491 $self->checkout_noncat;
1493 OpenILS::Event->new('SUCCESS',
1494 payload => { noncat_circ => $self->circ }));
1498 if( $self->is_precat ) {
1499 $self->make_precat_copy;
1500 return if $self->bail_out;
1502 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1503 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1506 $self->do_copy_checks;
1507 return if $self->bail_out;
1509 $self->run_checkout_scripts();
1510 return if $self->bail_out;
1512 $self->build_checkout_circ_object();
1513 return if $self->bail_out;
1515 my $modify_to_start = $self->booking_adjusted_due_date();
1516 return if $self->bail_out;
1518 $self->apply_modified_due_date($modify_to_start);
1519 return if $self->bail_out;
1521 return $self->bail_on_events($self->editor->event)
1522 unless $self->editor->create_action_circulation($self->circ);
1524 # refresh the circ to force local time zone for now
1525 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1527 if($self->limit_groups) {
1528 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1531 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1533 return if $self->bail_out;
1535 $self->apply_deposit_fee();
1536 return if $self->bail_out;
1538 $self->handle_checkout_holds();
1539 return if $self->bail_out;
1541 # ------------------------------------------------------------------------------
1542 # Update the patron penalty info in the DB. Run it for permit-overrides
1543 # since the penalties are not updated during the permit phase
1544 # ------------------------------------------------------------------------------
1545 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1547 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1550 if($self->is_renewal) {
1551 # flesh the billing summary for the checked-in circ
1552 $pcirc = $self->editor->retrieve_action_circulation([
1554 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1559 OpenILS::Event->new('SUCCESS',
1561 copy => $U->unflesh_copy($self->copy),
1562 volume => $self->volume,
1563 circ => $self->circ,
1565 holds_fulfilled => $self->fulfilled_holds,
1566 deposit_billing => $self->deposit_billing,
1567 rental_billing => $self->rental_billing,
1568 parent_circ => $pcirc,
1569 patron => ($self->return_patron) ? $self->patron : undef,
1570 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1576 sub apply_deposit_fee {
1578 my $copy = $self->copy;
1580 ($self->is_deposit and not $self->is_deposit_exempt) or
1581 ($self->is_rental and not $self->is_rental_exempt);
1583 return if $self->is_deposit and $self->skip_deposit_fee;
1584 return if $self->is_rental and $self->skip_rental_fee;
1586 my $bill = Fieldmapper::money::billing->new;
1587 my $amount = $copy->deposit_amount;
1591 if($self->is_deposit) {
1592 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1594 $self->deposit_billing($bill);
1596 $billing_type = OILS_BILLING_TYPE_RENTAL;
1598 $self->rental_billing($bill);
1601 $bill->xact($self->circ->id);
1602 $bill->amount($amount);
1603 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1604 $bill->billing_type($billing_type);
1605 $bill->btype($btype);
1606 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1608 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1613 my $copy = $self->copy;
1615 my $stat = $copy->status if ref $copy->status;
1616 my $loc = $copy->location if ref $copy->location;
1617 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1619 $copy->status($stat->id) if $stat;
1620 $copy->location($loc->id) if $loc;
1621 $copy->circ_lib($circ_lib->id) if $circ_lib;
1622 $copy->editor($self->editor->requestor->id);
1623 $copy->edit_date('now');
1624 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1626 return $self->bail_on_events($self->editor->event)
1627 unless $self->editor->update_asset_copy($self->copy);
1629 $copy->status($U->copy_status($copy->status));
1630 $copy->location($loc) if $loc;
1631 $copy->circ_lib($circ_lib) if $circ_lib;
1634 sub update_reservation {
1636 my $reservation = $self->reservation;
1638 my $usr = $reservation->usr;
1639 my $target_rt = $reservation->target_resource_type;
1640 my $target_r = $reservation->target_resource;
1641 my $current_r = $reservation->current_resource;
1643 $reservation->usr($usr->id) if ref $usr;
1644 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1645 $reservation->target_resource($target_r->id) if ref $target_r;
1646 $reservation->current_resource($current_r->id) if ref $current_r;
1648 return $self->bail_on_events($self->editor->event)
1649 unless $self->editor->update_booking_reservation($self->reservation);
1652 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1653 $self->reservation($reservation);
1657 sub bail_on_events {
1658 my( $self, @evts ) = @_;
1659 $self->push_events(@evts);
1663 # ------------------------------------------------------------------------------
1664 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1665 # affects copies that will fulfill holds and CIRC affects all other copies.
1666 # If blocks exists, bail, push Events onto the event pile, and return true.
1667 # ------------------------------------------------------------------------------
1668 sub check_hold_fulfill_blocks {
1671 # See if the user has any penalties applied that prevent hold fulfillment
1672 my $pens = $self->editor->json_query({
1673 select => {csp => ['name', 'label']},
1674 from => {ausp => {csp => {}}},
1677 usr => $self->patron->id,
1678 org_unit => $U->get_org_full_path($self->circ_lib),
1680 {stop_date => undef},
1681 {stop_date => {'>' => 'now'}}
1684 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1688 return 0 unless @$pens;
1690 for my $pen (@$pens) {
1691 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1692 my $event = OpenILS::Event->new($pen->{name});
1693 $event->{desc} = $pen->{label};
1694 $self->push_events($event);
1697 $self->override_events;
1698 return $self->bail_out;
1702 # ------------------------------------------------------------------------------
1703 # When an item is checked out, see if we can fulfill a hold for this patron
1704 # ------------------------------------------------------------------------------
1705 sub handle_checkout_holds {
1707 my $copy = $self->copy;
1708 my $patron = $self->patron;
1710 my $e = $self->editor;
1711 $self->fulfilled_holds([]);
1713 # pre/non-cats can't fulfill a hold
1714 return if $self->is_precat or $self->is_noncat;
1716 my $hold = $e->search_action_hold_request({
1717 current_copy => $copy->id ,
1718 cancel_time => undef,
1719 fulfillment_time => undef,
1721 {expire_time => undef},
1722 {expire_time => {'>' => 'now'}}
1726 if($hold and $hold->usr != $patron->id) {
1727 # reset the hold since the copy is now checked out
1729 $logger->info("circulator: un-targeting hold ".$hold->id.
1730 " because copy ".$copy->id." is getting checked out");
1732 $hold->clear_prev_check_time;
1733 $hold->clear_current_copy;
1734 $hold->clear_capture_time;
1735 $hold->clear_shelf_time;
1736 $hold->clear_shelf_expire_time;
1737 $hold->clear_current_shelf_lib;
1739 return $self->bail_on_event($e->event)
1740 unless $e->update_action_hold_request($hold);
1746 $hold = $self->find_related_user_hold($copy, $patron) or return;
1747 $logger->info("circulator: found related hold to fulfill in checkout");
1750 return if $self->check_hold_fulfill_blocks;
1752 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1754 # if the hold was never officially captured, capture it.
1755 $hold->current_copy($copy->id);
1756 $hold->capture_time('now') unless $hold->capture_time;
1757 $hold->fulfillment_time('now');
1758 $hold->fulfillment_staff($e->requestor->id);
1759 $hold->fulfillment_lib($self->circ_lib);
1761 return $self->bail_on_events($e->event)
1762 unless $e->update_action_hold_request($hold);
1764 $holdcode->delete_hold_copy_maps($e, $hold->id);
1765 return $self->fulfilled_holds([$hold->id]);
1769 # ------------------------------------------------------------------------------
1770 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1771 # the patron directly targets the checked out item, see if there is another hold
1772 # for the patron that could be fulfilled by the checked out item. Fulfill the
1773 # oldest hold and only fulfill 1 of them.
1775 # For "another hold":
1777 # First, check for one that the copy matches via hold_copy_map, ensuring that
1778 # *any* hold type that this copy could fill may end up filled.
1780 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1781 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1782 # that are non-requestable to count as capturing those hold types.
1783 # ------------------------------------------------------------------------------
1784 sub find_related_user_hold {
1785 my($self, $copy, $patron) = @_;
1786 my $e = $self->editor;
1788 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1790 return undef unless $U->ou_ancestor_setting_value(
1791 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1793 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1795 select => {ahr => ['id']},
1804 fkey => 'current_copy',
1805 type => 'left' # there may be no current_copy
1812 fulfillment_time => undef,
1813 cancel_time => undef,
1815 {expire_time => undef},
1816 {expire_time => {'>' => 'now'}}
1820 target_copy => $self->copy->id
1824 {id => undef}, # left-join copy may be nonexistent
1825 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1829 order_by => {ahr => {request_time => {direction => 'asc'}}},
1833 my $hold_info = $e->json_query($args)->[0];
1834 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1835 return undef if $U->ou_ancestor_setting_value(
1836 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1838 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1840 select => {ahr => ['id']},
1845 fkey => 'current_copy',
1846 type => 'left' # there may be no current_copy
1853 fulfillment_time => undef,
1854 cancel_time => undef,
1856 {expire_time => undef},
1857 {expire_time => {'>' => 'now'}}
1864 target => $self->volume->id
1870 target => $self->title->id
1876 {id => undef}, # left-join copy may be nonexistent
1877 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1881 order_by => {ahr => {request_time => {direction => 'asc'}}},
1885 $hold_info = $e->json_query($args)->[0];
1886 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1891 sub run_checkout_scripts {
1896 my $runner = $self->script_runner;
1905 my $hard_due_date_name;
1907 if(!$self->legacy_script_support) {
1908 $self->run_indb_circ_test();
1909 $duration = $self->circ_matrix_matchpoint->duration_rule;
1910 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1911 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1912 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1916 $runner->load($self->circ_duration);
1918 my $result = $runner->run or
1919 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1921 $duration_name = $result->{durationRule};
1922 $recurring_name = $result->{recurringFinesRule};
1923 $max_fine_name = $result->{maxFine};
1924 $hard_due_date_name = $result->{hardDueDate};
1927 $duration_name = $duration->name if $duration;
1928 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1931 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1932 return $self->bail_on_events($evt) if ($evt && !$nobail);
1934 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1935 return $self->bail_on_events($evt) if ($evt && !$nobail);
1937 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1938 return $self->bail_on_events($evt) if ($evt && !$nobail);
1940 if($hard_due_date_name) {
1941 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1942 return $self->bail_on_events($evt) if ($evt && !$nobail);
1948 # The item circulates with an unlimited duration
1952 $hard_due_date = undef;
1955 $self->duration_rule($duration);
1956 $self->recurring_fines_rule($recurring);
1957 $self->max_fine_rule($max_fine);
1958 $self->hard_due_date($hard_due_date);
1962 sub build_checkout_circ_object {
1965 my $circ = Fieldmapper::action::circulation->new;
1966 my $duration = $self->duration_rule;
1967 my $max = $self->max_fine_rule;
1968 my $recurring = $self->recurring_fines_rule;
1969 my $hard_due_date = $self->hard_due_date;
1970 my $copy = $self->copy;
1971 my $patron = $self->patron;
1972 my $duration_date_ceiling;
1973 my $duration_date_ceiling_force;
1977 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1978 $duration_date_ceiling = $policy->{duration_date_ceiling};
1979 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1981 my $dname = $duration->name;
1982 my $mname = $max->name;
1983 my $rname = $recurring->name;
1985 if($hard_due_date) {
1986 $hdname = $hard_due_date->name;
1989 $logger->debug("circulator: building circulation ".
1990 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1992 $circ->duration($policy->{duration});
1993 $circ->recurring_fine($policy->{recurring_fine});
1994 $circ->duration_rule($duration->name);
1995 $circ->recurring_fine_rule($recurring->name);
1996 $circ->max_fine_rule($max->name);
1997 $circ->max_fine($policy->{max_fine});
1998 $circ->fine_interval($recurring->recurrence_interval);
1999 $circ->renewal_remaining($duration->max_renewals);
2000 $circ->grace_period($policy->{grace_period});
2004 $logger->info("circulator: copy found with an unlimited circ duration");
2005 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2006 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2007 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2008 $circ->renewal_remaining(0);
2009 $circ->grace_period(0);
2012 $circ->target_copy( $copy->id );
2013 $circ->usr( $patron->id );
2014 $circ->circ_lib( $self->circ_lib );
2015 $circ->workstation($self->editor->requestor->wsid)
2016 if defined $self->editor->requestor->wsid;
2018 # renewals maintain a link to the parent circulation
2019 $circ->parent_circ($self->parent_circ);
2021 if( $self->is_renewal ) {
2022 $circ->opac_renewal('t') if $self->opac_renewal;
2023 $circ->phone_renewal('t') if $self->phone_renewal;
2024 $circ->desk_renewal('t') if $self->desk_renewal;
2025 $circ->renewal_remaining($self->renewal_remaining);
2026 $circ->circ_staff($self->editor->requestor->id);
2030 # if the user provided an overiding checkout time,
2031 # (e.g. the checkout really happened several hours ago), then
2032 # we apply that here. Does this need a perm??
2033 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2034 if $self->checkout_time;
2036 # if a patron is renewing, 'requestor' will be the patron
2037 $circ->circ_staff($self->editor->requestor->id);
2038 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
2043 sub do_reservation_pickup {
2046 $self->log_me("do_reservation_pickup()");
2048 $self->reservation->pickup_time('now');
2051 $self->reservation->current_resource &&
2052 $U->is_true($self->reservation->target_resource_type->catalog_item)
2054 # We used to try to set $self->copy and $self->patron here,
2055 # but that should already be done.
2057 $self->run_checkout_scripts(1);
2059 my $duration = $self->duration_rule;
2060 my $max = $self->max_fine_rule;
2061 my $recurring = $self->recurring_fines_rule;
2063 if ($duration && $max && $recurring) {
2064 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2066 my $dname = $duration->name;
2067 my $mname = $max->name;
2068 my $rname = $recurring->name;
2070 $logger->debug("circulator: updating reservation ".
2071 "with duration=$dname, maxfine=$mname, recurring=$rname");
2073 $self->reservation->fine_amount($policy->{recurring_fine});
2074 $self->reservation->max_fine($policy->{max_fine});
2075 $self->reservation->fine_interval($recurring->recurrence_interval);
2078 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2079 $self->update_copy();
2082 $self->reservation->fine_amount(
2083 $self->reservation->target_resource_type->fine_amount
2085 $self->reservation->max_fine(
2086 $self->reservation->target_resource_type->max_fine
2088 $self->reservation->fine_interval(
2089 $self->reservation->target_resource_type->fine_interval
2093 $self->update_reservation();
2096 sub do_reservation_return {
2098 my $request = shift;
2100 $self->log_me("do_reservation_return()");
2102 if (not ref $self->reservation) {
2103 my ($reservation, $evt) =
2104 $U->fetch_booking_reservation($self->reservation);
2105 return $self->bail_on_events($evt) if $evt;
2106 $self->reservation($reservation);
2109 $self->generate_fines(1);
2110 $self->reservation->return_time('now');
2111 $self->update_reservation();
2112 $self->reshelve_copy if $self->copy;
2114 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2115 $self->copy( $self->reservation->current_resource->catalog_item );
2119 sub booking_adjusted_due_date {
2121 my $circ = $self->circ;
2122 my $copy = $self->copy;
2124 return undef unless $self->use_booking;
2128 if( $self->due_date ) {
2130 return $self->bail_on_events($self->editor->event)
2131 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2133 $circ->due_date(cleanse_ISO8601($self->due_date));
2137 return unless $copy and $circ->due_date;
2140 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2141 if (@$booking_items) {
2142 my $booking_item = $booking_items->[0];
2143 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2145 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2146 my $shorten_circ_setting = $resource_type->elbow_room ||
2147 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2150 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2151 my $bookings = $booking_ses->request(
2152 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2153 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2155 $booking_ses->disconnect;
2157 my $dt_parser = DateTime::Format::ISO8601->new;
2158 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2160 for my $bid (@$bookings) {
2162 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2164 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2165 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2167 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2168 if ($booking_start < DateTime->now);
2171 if ($U->is_true($stop_circ_setting)) {
2172 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2174 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2175 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2178 # We set the circ duration here only to affect the logic that will
2179 # later (in a DB trigger) mangle the time part of the due date to
2180 # 11:59pm. Having any circ duration that is not a whole number of
2181 # days is enough to prevent the "correction."
2182 my $new_circ_duration = $due_date->epoch - time;
2183 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2184 $circ->duration("$new_circ_duration seconds");
2186 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2190 return $self->bail_on_events($self->editor->event)
2191 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2197 sub apply_modified_due_date {
2199 my $shift_earlier = shift;
2200 my $circ = $self->circ;
2201 my $copy = $self->copy;
2203 if( $self->due_date ) {
2205 return $self->bail_on_events($self->editor->event)
2206 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2208 $circ->due_date(cleanse_ISO8601($self->due_date));
2212 # if the due_date lands on a day when the location is closed
2213 return unless $copy and $circ->due_date;
2215 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2217 # due-date overlap should be determined by the location the item
2218 # is checked out from, not the owning or circ lib of the item
2219 my $org = $self->circ_lib;
2221 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2222 " with an item due date of ".$circ->due_date );
2224 my $dateinfo = $U->storagereq(
2225 'open-ils.storage.actor.org_unit.closed_date.overlap',
2226 $org, $circ->due_date );
2229 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2230 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2232 # XXX make the behavior more dynamic
2233 # for now, we just push the due date to after the close date
2234 if ($shift_earlier) {
2235 $circ->due_date($dateinfo->{start});
2237 $circ->due_date($dateinfo->{end});
2245 sub create_due_date {
2246 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2248 # if there is a raw time component (e.g. from postgres),
2249 # turn it into an interval that interval_to_seconds can parse
2250 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2252 # for now, use the server timezone. TODO: use workstation org timezone
2253 my $due_date = DateTime->now(time_zone => 'local');
2255 # add the circ duration
2256 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2259 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2260 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2261 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2266 # return ISO8601 time with timezone
2267 return $due_date->strftime('%FT%T%z');
2272 sub make_precat_copy {
2274 my $copy = $self->copy;
2277 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2279 $copy->editor($self->editor->requestor->id);
2280 $copy->edit_date('now');
2281 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2282 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2283 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2284 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2285 $self->update_copy();
2289 $logger->info("circulator: Creating a new precataloged ".
2290 "copy in checkout with barcode " . $self->copy_barcode);
2292 $copy = Fieldmapper::asset::copy->new;
2293 $copy->circ_lib($self->circ_lib);
2294 $copy->creator($self->editor->requestor->id);
2295 $copy->editor($self->editor->requestor->id);
2296 $copy->barcode($self->copy_barcode);
2297 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2298 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2299 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2301 $copy->dummy_title($self->dummy_title || "");
2302 $copy->dummy_author($self->dummy_author || "");
2303 $copy->dummy_isbn($self->dummy_isbn || "");
2304 $copy->circ_modifier($self->circ_modifier);
2307 # See if we need to override the circ_lib for the copy with a configured circ_lib
2308 # Setting is shortname of the org unit
2309 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2310 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2312 if($precat_circ_lib) {
2313 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2316 $self->bail_on_events($self->editor->event);
2320 $copy->circ_lib($org->id);
2324 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2326 $self->push_events($self->editor->event);
2330 # this is a little bit of a hack, but we need to
2331 # get the copy into the script runner
2332 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2336 sub checkout_noncat {
2342 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2343 my $count = $self->noncat_count || 1;
2344 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2346 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2350 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2351 $self->editor->requestor->id,
2359 $self->push_events($evt);
2367 # If a copy goes into transit and is then checked in before the transit checkin
2368 # interval has expired, push an event onto the overridable events list.
2369 sub check_transit_checkin_interval {
2372 # only concerned with in-transit items
2373 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2375 # no interval, no problem
2376 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2377 return unless $interval;
2379 # capture the transit so we don't have to fetch it again later during checkin
2381 $self->editor->search_action_transit_copy(
2382 {target_copy => $self->copy->id, dest_recv_time => undef}
2386 # transit from X to X for whatever reason has no min interval
2387 return if $self->transit->source == $self->transit->dest;
2389 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2390 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2391 my $horizon = $t_start->add(seconds => $seconds);
2393 # See if we are still within the transit checkin forbidden range
2394 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2395 if $horizon > DateTime->now;
2398 # Retarget local holds at checkin
2399 sub checkin_retarget {
2401 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2402 return unless $self->is_checkin; # Renewals need not be checked
2403 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2404 return if $self->is_precat; # No holds for precats
2405 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2406 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2407 my $status = $U->copy_status($self->copy->status);
2408 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2409 # Specifically target items that are likely new (by status ID)
2410 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2411 my $location = $self->copy->location;
2412 if(!ref($location)) {
2413 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2414 $self->copy->location($location);
2416 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2418 # Fetch holds for the bib
2419 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2420 $self->editor->authtoken,
2423 capture_time => undef, # No touching captured holds
2424 frozen => 'f', # Don't bother with frozen holds
2425 pickup_lib => $self->circ_lib # Only holds actually here
2428 # Error? Skip the step.
2429 return if exists $result->{"ilsevent"};
2433 foreach my $holdlist (keys %{$result}) {
2434 push @$holds, @{$result->{$holdlist}};
2437 return if scalar(@$holds) == 0; # No holds, no retargeting
2439 # Check for parts on this copy
2440 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2441 my %parts_hash = ();
2442 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2444 # Loop over holds in request-ish order
2445 # Stage 1: Get them into request-ish order
2446 # Also grab type and target for skipping low hanging ones
2447 $result = $self->editor->json_query({
2448 "select" => { "ahr" => ["id", "hold_type", "target"] },
2449 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2450 "where" => { "id" => $holds },
2452 { "class" => "pgt", "field" => "hold_priority"},
2453 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2454 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2455 { "class" => "ahr", "field" => "request_time"}
2460 if (ref $result eq "ARRAY" and scalar @$result) {
2461 foreach (@{$result}) {
2462 # Copy level, but not this copy?
2463 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2464 and $_->{target} != $self->copy->id);
2465 # Volume level, but not this volume?
2466 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2467 if(@$parts) { # We have parts?
2469 next if ($_->{hold_type} eq 'T');
2470 # Skip part holds for parts not on this copy
2471 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2473 # No parts, no part holds
2474 next if ($_->{hold_type} eq 'P');
2476 # So much for easy stuff, attempt a retarget!
2477 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2478 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2479 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2487 $self->log_me("do_checkin()");
2489 return $self->bail_on_events(
2490 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2493 $self->check_transit_checkin_interval;
2494 $self->checkin_retarget;
2496 # the renew code and mk_env should have already found our circulation object
2497 unless( $self->circ ) {
2499 my $circs = $self->editor->search_action_circulation(
2500 { target_copy => $self->copy->id, checkin_time => undef });
2502 $self->circ($$circs[0]);
2504 # for now, just warn if there are multiple open circs on a copy
2505 $logger->warn("circulator: we have ".scalar(@$circs).
2506 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2509 # run the fine generator against this circ, if this circ is there
2510 $self->generate_fines_start if $self->circ;
2512 if( $self->checkin_check_holds_shelf() ) {
2513 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2514 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2515 if($self->fake_hold_dest) {
2516 $self->hold->pickup_lib($self->circ_lib);
2518 $self->checkin_flesh_events;
2522 unless( $self->is_renewal ) {
2523 return $self->bail_on_events($self->editor->event)
2524 unless $self->editor->allowed('COPY_CHECKIN');
2527 $self->push_events($self->check_copy_alert());
2528 $self->push_events($self->check_checkin_copy_status());
2530 # if the circ is marked as 'claims returned', add the event to the list
2531 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2532 if ($self->circ and $self->circ->stop_fines
2533 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2535 $self->check_circ_deposit();
2537 # handle the overridable events
2538 $self->override_events unless $self->is_renewal;
2539 return if $self->bail_out;
2541 if( $self->copy and !$self->transit ) {
2543 $self->editor->search_action_transit_copy(
2544 { target_copy => $self->copy->id, dest_recv_time => undef }
2550 $self->generate_fines_finish;
2551 $self->checkin_handle_circ;
2552 return if $self->bail_out;
2553 $self->checkin_changed(1);
2555 } elsif( $self->transit ) {
2556 my $hold_transit = $self->process_received_transit;
2557 $self->checkin_changed(1);
2559 if( $self->bail_out ) {
2560 $self->checkin_flesh_events;
2564 if( my $e = $self->check_checkin_copy_status() ) {
2565 # If the original copy status is special, alert the caller
2566 my $ev = $self->events;
2567 $self->events([$e]);
2568 $self->override_events;
2569 return if $self->bail_out;
2573 if( $hold_transit or
2574 $U->copy_status($self->copy->status)->id
2575 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2578 if( $hold_transit ) {
2579 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2581 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2586 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2588 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2589 $self->reshelve_copy(1);
2590 $self->cancelled_hold_transit(1);
2591 $self->notify_hold(0); # don't notify for cancelled holds
2592 $self->fake_hold_dest(0);
2593 return if $self->bail_out;
2595 } elsif ($hold and $hold->hold_type eq 'R') {
2597 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2598 $self->notify_hold(0); # No need to notify
2599 $self->fake_hold_dest(0);
2600 $self->noop(1); # Don't try and capture for other holds/transits now
2601 $self->update_copy();
2602 $hold->fulfillment_time('now');
2603 $self->bail_on_events($self->editor->event)
2604 unless $self->editor->update_action_hold_request($hold);
2608 # hold transited to correct location
2609 if($self->fake_hold_dest) {
2610 $hold->pickup_lib($self->circ_lib);
2612 $self->checkin_flesh_events;
2617 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2619 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2620 " that is in-transit, but there is no transit.. repairing");
2621 $self->reshelve_copy(1);
2622 return if $self->bail_out;
2625 if( $self->is_renewal ) {
2626 $self->finish_fines_and_voiding;
2627 return if $self->bail_out;
2628 $self->push_events(OpenILS::Event->new('SUCCESS'));
2632 # ------------------------------------------------------------------------------
2633 # Circulations and transits are now closed where necessary. Now go on to see if
2634 # this copy can fulfill a hold or needs to be routed to a different location
2635 # ------------------------------------------------------------------------------
2637 my $needed_for_something = 0; # formerly "needed_for_hold"
2639 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2641 if (!$self->remote_hold) {
2642 if ($self->use_booking) {
2643 my $potential_hold = $self->hold_capture_is_possible;
2644 my $potential_reservation = $self->reservation_capture_is_possible;
2646 if ($potential_hold and $potential_reservation) {
2647 $logger->info("circulator: item could fulfill either hold or reservation");
2648 $self->push_events(new OpenILS::Event(
2649 "HOLD_RESERVATION_CONFLICT",
2650 "hold" => $potential_hold,
2651 "reservation" => $potential_reservation
2653 return if $self->bail_out;
2654 } elsif ($potential_hold) {
2655 $needed_for_something =
2656 $self->attempt_checkin_hold_capture;
2657 } elsif ($potential_reservation) {
2658 $needed_for_something =
2659 $self->attempt_checkin_reservation_capture;
2662 $needed_for_something = $self->attempt_checkin_hold_capture;
2665 return if $self->bail_out;
2667 unless($needed_for_something) {
2668 my $circ_lib = (ref $self->copy->circ_lib) ?
2669 $self->copy->circ_lib->id : $self->copy->circ_lib;
2671 if( $self->remote_hold ) {
2672 $circ_lib = $self->remote_hold->pickup_lib;
2673 $logger->warn("circulator: Copy ".$self->copy->barcode.
2674 " is on a remote hold's shelf, sending to $circ_lib");
2677 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2679 my $suppress_transit = 0;
2681 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2682 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2683 if($suppress_transit_source && $suppress_transit_source->{value}) {
2684 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2685 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2686 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2687 $suppress_transit = 1;
2692 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2693 # copy is where it needs to be, either for hold or reshelving
2695 $self->checkin_handle_precat();
2696 return if $self->bail_out;
2699 # copy needs to transit "home", or stick here if it's a floating copy
2701 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2702 $self->checkin_changed(1);
2703 $self->copy->circ_lib( $self->circ_lib );
2706 my $bc = $self->copy->barcode;
2707 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2708 $self->checkin_build_copy_transit($circ_lib);
2709 return if $self->bail_out;
2710 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2714 } else { # no-op checkin
2715 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2716 $self->checkin_changed(1);
2717 $self->copy->circ_lib( $self->circ_lib );
2722 if($self->claims_never_checked_out and
2723 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2725 # the item was not supposed to be checked out to the user and should now be marked as missing
2726 $self->copy->status(OILS_COPY_STATUS_MISSING);
2730 $self->reshelve_copy unless $needed_for_something;
2733 return if $self->bail_out;
2735 unless($self->checkin_changed) {
2737 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2738 my $stat = $U->copy_status($self->copy->status)->id;
2740 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2741 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2742 $self->bail_out(1); # no need to commit anything
2746 $self->push_events(OpenILS::Event->new('SUCCESS'))
2747 unless @{$self->events};
2750 $self->finish_fines_and_voiding;
2752 OpenILS::Utils::Penalty->calculate_penalties(
2753 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2755 $self->checkin_flesh_events;
2759 sub finish_fines_and_voiding {
2761 return unless $self->circ;
2763 # gather any updates to the circ after fine generation, if there was a circ
2764 $self->generate_fines_finish;
2766 return unless $self->backdate or $self->void_overdues;
2768 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2769 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2771 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2772 $self->editor, $self->circ, $self->backdate, $note);
2774 return $self->bail_on_events($evt) if $evt;
2776 # make sure the circ isn't closed if we just voided some fines
2777 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2778 return $self->bail_on_events($evt) if $evt;
2784 # if a deposit was payed for this item, push the event
2785 sub check_circ_deposit {
2787 return unless $self->circ;
2788 my $deposit = $self->editor->search_money_billing(
2790 xact => $self->circ->id,
2792 }, {idlist => 1})->[0];
2794 $self->push_events(OpenILS::Event->new(
2795 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2800 my $force = $self->force || shift;
2801 my $copy = $self->copy;
2803 my $stat = $U->copy_status($copy->status)->id;
2806 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2807 $stat != OILS_COPY_STATUS_CATALOGING and
2808 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2809 $stat != OILS_COPY_STATUS_RESHELVING )) {
2811 $copy->status( OILS_COPY_STATUS_RESHELVING );
2813 $self->checkin_changed(1);
2818 # Returns true if the item is at the current location
2819 # because it was transited there for a hold and the
2820 # hold has not been fulfilled
2821 sub checkin_check_holds_shelf {
2823 return 0 unless $self->copy;
2826 $U->copy_status($self->copy->status)->id ==
2827 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2829 # Attempt to clear shelf expired holds for this copy
2830 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2831 if($self->clear_expired);
2833 # find the hold that put us on the holds shelf
2834 my $holds = $self->editor->search_action_hold_request(
2836 current_copy => $self->copy->id,
2837 capture_time => { '!=' => undef },
2838 fulfillment_time => undef,
2839 cancel_time => undef,
2844 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2845 $self->reshelve_copy(1);
2849 my $hold = $$holds[0];
2851 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2852 $hold->id. "] for copy ".$self->copy->barcode);
2854 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2855 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2856 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2857 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2858 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2859 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2860 $self->fake_hold_dest(1);
2866 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2867 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2871 $logger->info("circulator: hold is not for here..");
2872 $self->remote_hold($hold);
2877 sub checkin_handle_precat {
2879 my $copy = $self->copy;
2881 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2882 $copy->status(OILS_COPY_STATUS_CATALOGING);
2883 $self->update_copy();
2884 $self->checkin_changed(1);
2885 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2890 sub checkin_build_copy_transit {
2893 my $copy = $self->copy;
2894 my $transit = Fieldmapper::action::transit_copy->new;
2896 # if we are transiting an item to the shelf shelf, it's a hold transit
2897 if (my $hold = $self->remote_hold) {
2898 $transit = Fieldmapper::action::hold_transit_copy->new;
2899 $transit->hold($hold->id);
2901 # the item is going into transit, remove any shelf-iness
2902 if ($hold->current_shelf_lib or $hold->shelf_time) {
2903 $hold->clear_current_shelf_lib;
2904 $hold->clear_shelf_time;
2905 return $self->bail_on_events($self->editor->event)
2906 unless $self->editor->update_action_hold_request($hold);
2910 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2911 $logger->info("circulator: transiting copy to $dest");
2913 $transit->source($self->circ_lib);
2914 $transit->dest($dest);
2915 $transit->target_copy($copy->id);
2916 $transit->source_send_time('now');
2917 $transit->copy_status( $U->copy_status($copy->status)->id );
2919 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2921 if ($self->remote_hold) {
2922 return $self->bail_on_events($self->editor->event)
2923 unless $self->editor->create_action_hold_transit_copy($transit);
2925 return $self->bail_on_events($self->editor->event)
2926 unless $self->editor->create_action_transit_copy($transit);
2929 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2931 $self->checkin_changed(1);
2935 sub hold_capture_is_possible {
2937 my $copy = $self->copy;
2939 # we've been explicitly told not to capture any holds
2940 return 0 if $self->capture eq 'nocapture';
2942 # See if this copy can fulfill any holds
2943 my $hold = $holdcode->find_nearest_permitted_hold(
2944 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2946 return undef if ref $hold eq "HASH" and
2947 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2951 sub reservation_capture_is_possible {
2953 my $copy = $self->copy;
2955 # we've been explicitly told not to capture any holds
2956 return 0 if $self->capture eq 'nocapture';
2958 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2959 my $resv = $booking_ses->request(
2960 "open-ils.booking.reservations.could_capture",
2961 $self->editor->authtoken, $copy->barcode
2963 $booking_ses->disconnect;
2964 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2965 $self->push_events($resv);
2971 # returns true if the item was used (or may potentially be used
2972 # in subsequent calls) to capture a hold.
2973 sub attempt_checkin_hold_capture {
2975 my $copy = $self->copy;
2977 # we've been explicitly told not to capture any holds
2978 return 0 if $self->capture eq 'nocapture';
2980 # See if this copy can fulfill any holds
2981 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2982 $self->editor, $copy, $self->editor->requestor );
2985 $logger->debug("circulator: no potential permitted".
2986 "holds found for copy ".$copy->barcode);
2990 if($self->capture ne 'capture') {
2991 # see if this item is in a hold-capture-delay location
2992 my $location = $self->copy->location;
2993 if(!ref($location)) {
2994 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2995 $self->copy->location($location);
2997 if($U->is_true($location->hold_verify)) {
2998 $self->bail_on_events(
2999 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3004 $self->retarget($retarget);
3006 my $suppress_transit = 0;
3007 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3008 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3009 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3010 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3011 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3012 $suppress_transit = 1;
3013 $self->hold->pickup_lib($self->circ_lib);
3018 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3020 $hold->current_copy($copy->id);
3021 $hold->capture_time('now');
3022 $self->put_hold_on_shelf($hold)
3023 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3025 # prevent DB errors caused by fetching
3026 # holds from storage, and updating through cstore
3027 $hold->clear_fulfillment_time;
3028 $hold->clear_fulfillment_staff;
3029 $hold->clear_fulfillment_lib;
3030 $hold->clear_expire_time;
3031 $hold->clear_cancel_time;
3032 $hold->clear_prev_check_time unless $hold->prev_check_time;
3034 $self->bail_on_events($self->editor->event)
3035 unless $self->editor->update_action_hold_request($hold);
3037 $self->checkin_changed(1);
3039 return 0 if $self->bail_out;
3041 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3043 if ($hold->hold_type eq 'R') {
3044 $copy->status(OILS_COPY_STATUS_CATALOGING);
3045 $hold->fulfillment_time('now');
3046 $self->noop(1); # Block other transit/hold checks
3047 $self->bail_on_events($self->editor->event)
3048 unless $self->editor->update_action_hold_request($hold);
3050 # This hold was captured in the correct location
3051 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3052 $self->push_events(OpenILS::Event->new('SUCCESS'));
3054 #$self->do_hold_notify($hold->id);
3055 $self->notify_hold($hold->id);
3060 # Hold needs to be picked up elsewhere. Build a hold
3061 # transit and route the item.
3062 $self->checkin_build_hold_transit();
3063 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3064 return 0 if $self->bail_out;
3065 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3068 # make sure we save the copy status
3070 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3074 sub attempt_checkin_reservation_capture {
3076 my $copy = $self->copy;
3078 # we've been explicitly told not to capture any holds
3079 return 0 if $self->capture eq 'nocapture';
3081 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3082 my $evt = $booking_ses->request(
3083 "open-ils.booking.resources.capture_for_reservation",
3084 $self->editor->authtoken,
3086 1 # don't update copy - we probably have it locked
3088 $booking_ses->disconnect;
3090 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3092 "open-ils.booking.resources.capture_for_reservation " .
3093 "didn't return an event!"
3097 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3098 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3100 # not-transferable is an error event we'll pass on the user
3101 $logger->warn("reservation capture attempted against non-transferable item");
3102 $self->push_events($evt);
3104 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3105 # Re-retrieve copy as reservation capture may have changed
3106 # its status and whatnot.
3108 "circulator: booking capture win on copy " . $self->copy->id
3110 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3112 "circulator: changing copy " . $self->copy->id .
3113 "'s status from " . $self->copy->status . " to " .
3116 $self->copy->status($new_copy_status);
3119 $self->reservation($evt->{"payload"}->{"reservation"});
3121 if (exists $evt->{"payload"}->{"transit"}) {
3125 "org" => $evt->{"payload"}->{"transit"}->dest
3129 $self->checkin_changed(1);
3133 # other results are treated as "nothing to capture"
3137 sub do_hold_notify {
3138 my( $self, $holdid ) = @_;
3140 my $e = new_editor(xact => 1);
3141 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3143 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3144 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3146 $logger->info("circulator: running delayed hold notify process");
3148 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3149 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3151 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3152 hold_id => $holdid, requestor => $self->editor->requestor);
3154 $logger->debug("circulator: built hold notifier");
3156 if(!$notifier->event) {
3158 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3160 my $stat = $notifier->send_email_notify;
3161 if( $stat == '1' ) {
3162 $logger->info("circulator: hold notify succeeded for hold $holdid");
3166 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3169 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3173 sub retarget_holds {
3175 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3176 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3177 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3178 # no reason to wait for the return value
3182 sub checkin_build_hold_transit {
3185 my $copy = $self->copy;
3186 my $hold = $self->hold;
3187 my $trans = Fieldmapper::action::hold_transit_copy->new;
3189 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3191 $trans->hold($hold->id);
3192 $trans->source($self->circ_lib);
3193 $trans->dest($hold->pickup_lib);
3194 $trans->source_send_time("now");
3195 $trans->target_copy($copy->id);
3197 # when the copy gets to its destination, it will recover
3198 # this status - put it onto the holds shelf
3199 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3201 return $self->bail_on_events($self->editor->event)
3202 unless $self->editor->create_action_hold_transit_copy($trans);
3207 sub process_received_transit {
3209 my $copy = $self->copy;
3210 my $copyid = $self->copy->id;
3212 my $status_name = $U->copy_status($copy->status)->name;
3213 $logger->debug("circulator: attempting transit receive on ".
3214 "copy $copyid. Copy status is $status_name");
3216 my $transit = $self->transit;
3218 # Check if we are in a transit suppress range
3219 my $suppress_transit = 0;
3220 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3221 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3222 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3223 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3224 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3225 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3226 $suppress_transit = 1;
3227 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3231 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3232 # - this item is in-transit to a different location
3233 # - Or we are capturing holds as transits, so why create a new transit?
3235 my $tid = $transit->id;
3236 my $loc = $self->circ_lib;
3237 my $dest = $transit->dest;
3239 $logger->info("circulator: Fowarding transit on copy which is destined ".
3240 "for a different location. transit=$tid, copy=$copyid, current ".
3241 "location=$loc, destination location=$dest");
3243 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3245 # grab the associated hold object if available
3246 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3247 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3249 return $self->bail_on_events($evt);
3252 # The transit is received, set the receive time
3253 $transit->dest_recv_time('now');
3254 $self->bail_on_events($self->editor->event)
3255 unless $self->editor->update_action_transit_copy($transit);
3257 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3259 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3260 $copy->status( $transit->copy_status );
3261 $self->update_copy();
3262 return if $self->bail_out;
3266 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3268 # hold has arrived at destination, set shelf time
3269 $self->put_hold_on_shelf($hold);
3270 $self->bail_on_events($self->editor->event)
3271 unless $self->editor->update_action_hold_request($hold);
3272 return if $self->bail_out;
3274 $self->notify_hold($hold_transit->hold);
3279 OpenILS::Event->new(
3282 payload => { transit => $transit, holdtransit => $hold_transit } ));
3284 return $hold_transit;
3288 # ------------------------------------------------------------------
3289 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3290 # ------------------------------------------------------------------
3291 sub put_hold_on_shelf {
3292 my($self, $hold) = @_;
3293 $hold->shelf_time('now');
3294 $hold->current_shelf_lib($self->circ_lib);
3295 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3301 sub generate_fines {
3303 my $reservation = shift;
3305 $self->generate_fines_start($reservation);
3306 $self->generate_fines_finish($reservation);
3311 sub generate_fines_start {
3313 my $reservation = shift;
3314 my $dt_parser = DateTime::Format::ISO8601->new;
3316 my $obj = $reservation ? $self->reservation : $self->circ;
3318 # If we have a grace period
3319 if($obj->can('grace_period')) {
3320 # Parse out the due date
3321 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3322 # Add the grace period to the due date
3323 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3324 # Don't generate fines on circs still in grace period
3325 return undef if ($due_date > DateTime->now);
3328 if (!exists($self->{_gen_fines_req})) {
3329 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3331 'open-ils.storage.action.circulation.overdue.generate_fines',
3339 sub generate_fines_finish {
3341 my $reservation = shift;
3343 return undef unless $self->{_gen_fines_req};
3345 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3347 $self->{_gen_fines_req}->wait_complete;
3348 delete($self->{_gen_fines_req});
3350 # refresh the circ in case the fine generator set the stop_fines field
3351 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3352 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3357 sub checkin_handle_circ {
3359 my $circ = $self->circ;
3360 my $copy = $self->copy;
3364 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3366 # backdate the circ if necessary
3367 if($self->backdate) {
3368 my $evt = $self->checkin_handle_backdate;
3369 return $self->bail_on_events($evt) if $evt;
3372 if(!$circ->stop_fines) {
3373 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3374 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3375 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3376 $circ->stop_fines_time('now');
3377 $circ->stop_fines_time($self->backdate) if $self->backdate;
3380 # Set the checkin vars since we have the item
3381 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3383 # capture the true scan time for back-dated checkins
3384 $circ->checkin_scan_time('now');
3386 $circ->checkin_staff($self->editor->requestor->id);
3387 $circ->checkin_lib($self->circ_lib);
3388 $circ->checkin_workstation($self->editor->requestor->wsid);
3390 my $circ_lib = (ref $self->copy->circ_lib) ?
3391 $self->copy->circ_lib->id : $self->copy->circ_lib;
3392 my $stat = $U->copy_status($self->copy->status)->id;
3394 if ($stat == OILS_COPY_STATUS_LOST) {
3395 # we will now handle lost fines, but the copy will retain its 'lost'
3396 # status if it needs to transit home unless lost_immediately_available
3399 # if we decide to also delay fine handling until the item arrives home,
3400 # we will need to call lost fine handling code both when checking items
3401 # in and also when receiving transits
3402 $self->checkin_handle_lost($circ_lib);
3403 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3404 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3406 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3411 # see if there are any fines owed on this circ. if not, close it
3412 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3413 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3415 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3417 return $self->bail_on_events($self->editor->event)
3418 unless $self->editor->update_action_circulation($circ);
3424 # ------------------------------------------------------------------
3425 # See if we need to void billings for lost checkin
3426 # ------------------------------------------------------------------
3427 sub checkin_handle_lost {
3429 my $circ_lib = shift;
3430 my $circ = $self->circ;
3432 my $max_return = $U->ou_ancestor_setting_value(
3433 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3438 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3439 $tm[5] -= 1 if $tm[5] > 0;
3440 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3442 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3443 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3445 $max_return = 0 if $today < $last_chance;
3448 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3450 my $void_lost = $U->ou_ancestor_setting_value(
3451 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3452 my $void_lost_fee = $U->ou_ancestor_setting_value(
3453 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3454 my $restore_od = $U->ou_ancestor_setting_value(
3455 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3456 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3457 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3459 $self->checkin_handle_lost_now_found(3) if $void_lost;
3460 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3461 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3464 if ($circ_lib != $self->circ_lib) {
3465 # if the item is not home, check to see if we want to retain the lost
3466 # status at this point in the process
3467 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3469 if ($immediately_available) {
3470 # lost item status does not need to be retained, so give it a
3471 # reshelving status as if it were a normal checkin
3472 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3475 $logger->info("circulator: not updating copy status on checkin because copy is lost");
3478 # lost item is home and processed, treat like a normal checkin from
3480 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3486 sub checkin_handle_backdate {
3489 # ------------------------------------------------------------------
3490 # clean up the backdate for date comparison
3491 # XXX We are currently taking the due-time from the original due-date,
3492 # not the input. Do we need to do this? This certainly interferes with
3493 # backdating of hourly checkouts, but that is likely a very rare case.
3494 # ------------------------------------------------------------------
3495 my $bd = cleanse_ISO8601($self->backdate);
3496 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3497 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3498 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
3500 $self->backdate($bd);
3505 sub check_checkin_copy_status {
3507 my $copy = $self->copy;
3509 my $status = $U->copy_status($copy->status)->id;
3512 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3513 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3514 $status == OILS_COPY_STATUS_IN_PROCESS ||
3515 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3516 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3517 $status == OILS_COPY_STATUS_CATALOGING ||
3518 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3519 $status == OILS_COPY_STATUS_RESHELVING );
3521 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3522 if( $status == OILS_COPY_STATUS_LOST );
3524 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3525 if( $status == OILS_COPY_STATUS_MISSING );
3527 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3532 # --------------------------------------------------------------------------
3533 # On checkin, we need to return as many relevant objects as we can
3534 # --------------------------------------------------------------------------
3535 sub checkin_flesh_events {
3538 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3539 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3540 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3543 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3546 if($self->hold and !$self->hold->cancel_time) {
3547 $hold = $self->hold;
3548 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3552 # if we checked in a circulation, flesh the billing summary data
3553 $self->circ->billable_transaction(
3554 $self->editor->retrieve_money_billable_transaction([
3556 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3562 # flesh some patron fields before returning
3564 $self->editor->retrieve_actor_user([
3569 au => ['card', 'billing_address', 'mailing_address']
3576 for my $evt (@{$self->events}) {
3579 $payload->{copy} = $U->unflesh_copy($self->copy);
3580 $payload->{volume} = $self->volume;
3581 $payload->{record} = $record,
3582 $payload->{circ} = $self->circ;
3583 $payload->{transit} = $self->transit;
3584 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3585 $payload->{hold} = $hold;
3586 $payload->{patron} = $self->patron;
3587 $payload->{reservation} = $self->reservation
3588 unless (not $self->reservation or $self->reservation->cancel_time);
3590 $evt->{payload} = $payload;
3595 my( $self, $msg ) = @_;
3596 my $bc = ($self->copy) ? $self->copy->barcode :
3599 my $usr = ($self->patron) ? $self->patron->id : "";
3600 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3601 ", recipient=$usr, copy=$bc");
3607 $self->log_me("do_renew()");
3609 # Make sure there is an open circ to renew that is not
3610 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3611 my $usrid = $self->patron->id if $self->patron;
3612 my $circ = $self->editor->search_action_circulation({
3613 target_copy => $self->copy->id,
3614 xact_finish => undef,
3615 checkin_time => undef,
3616 ($usrid ? (usr => $usrid) : ()),
3618 {stop_fines => undef},
3619 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3623 return $self->bail_on_events($self->editor->event) unless $circ;
3625 # A user is not allowed to renew another user's items without permission
3626 unless( $circ->usr eq $self->editor->requestor->id ) {
3627 return $self->bail_on_events($self->editor->events)
3628 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3631 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3632 if $circ->renewal_remaining < 1;
3634 # -----------------------------------------------------------------
3636 $self->parent_circ($circ->id);
3637 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3640 # Opac renewal - re-use circ library from original circ (unless told not to)
3641 if($self->opac_renewal) {
3642 unless(defined($opac_renewal_use_circ_lib)) {
3643 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3644 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3645 $opac_renewal_use_circ_lib = 1;
3648 $opac_renewal_use_circ_lib = 0;
3651 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3654 # Run the fine generator against the old circ
3655 $self->generate_fines_start;
3657 $self->run_renew_permit;
3660 $self->do_checkin();
3661 return if $self->bail_out;
3663 unless( $self->permit_override ) {
3665 return if $self->bail_out;
3666 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3667 $self->remove_event('ITEM_NOT_CATALOGED');
3670 $self->override_events;
3671 return if $self->bail_out;
3674 $self->do_checkout();
3679 my( $self, $evt ) = @_;
3680 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3681 $logger->debug("circulator: removing event from list: $evt");
3682 my @events = @{$self->events};
3683 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3688 my( $self, $evt ) = @_;
3689 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3690 return grep { $_->{textcode} eq $evt } @{$self->events};
3695 sub run_renew_permit {
3698 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3699 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3700 $self->editor, $self->copy, $self->editor->requestor, 1
3702 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3705 if(!$self->legacy_script_support) {
3706 my $results = $self->run_indb_circ_test;
3707 $self->push_events($self->matrix_test_result_events)
3708 unless $self->circ_test_success;
3711 my $runner = $self->script_runner;
3713 $runner->load($self->circ_permit_renew);
3714 my $result = $runner->run or
3715 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3716 if ($result->{"events"}) {
3718 map { new OpenILS::Event($_) } @{$result->{"events"}}
3721 "circulator: circ_permit_renew for user " .
3722 $self->patron->id . " returned " .
3723 scalar(@{$result->{"events"}}) . " event(s)"
3727 $self->mk_script_runner;
3730 $logger->debug("circulator: re-creating script runner to be safe");
3734 # XXX: The primary mechanism for storing circ history is now handled
3735 # by tracking real circulation objects instead of bibs in a bucket.
3736 # However, this code is disabled by default and could be useful
3737 # some day, so may as well leave it for now.
3738 sub append_reading_list {
3742 $self->is_checkout and
3748 # verify history is globally enabled and uses the bucket mechanism
3749 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3750 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3752 return undef unless $htype and $htype eq 'bucket';
3754 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3756 # verify the patron wants to retain the hisory
3757 my $setting = $e->search_actor_user_setting(
3758 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3760 unless($setting and $setting->value) {
3765 my $bkt = $e->search_container_copy_bucket(
3766 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3771 # find the next item position
3772 my $last_item = $e->search_container_copy_bucket_item(
3773 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3774 $pos = $last_item->pos + 1 if $last_item;
3777 # create the history bucket if necessary
3778 $bkt = Fieldmapper::container::copy_bucket->new;
3779 $bkt->owner($self->patron->id);
3781 $bkt->btype('circ_history');
3783 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3786 my $item = Fieldmapper::container::copy_bucket_item->new;
3788 $item->bucket($bkt->id);
3789 $item->target_copy($self->copy->id);
3792 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3799 sub make_trigger_events {
3801 return unless $self->circ;
3802 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3803 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3804 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3809 sub checkin_handle_lost_now_found {
3810 my ($self, $bill_type) = @_;
3812 # ------------------------------------------------------------------
3813 # remove charge from patron's account if lost item is returned
3814 # ------------------------------------------------------------------
3816 my $bills = $self->editor->search_money_billing(
3818 xact => $self->circ->id,
3823 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3824 for my $bill (@$bills) {
3825 if( !$U->is_true($bill->voided) ) {
3826 $logger->info("lost item returned - voiding bill ".$bill->id);
3828 $bill->void_time('now');
3829 $bill->voider($self->editor->requestor->id);
3830 my $note = ($bill->note) ? $bill->note . "\n" : '';
3831 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3833 $self->bail_on_events($self->editor->event)
3834 unless $self->editor->update_money_billing($bill);
3839 sub checkin_handle_lost_now_found_restore_od {
3841 my $circ_lib = shift;
3843 # ------------------------------------------------------------------
3844 # restore those overdue charges voided when item was set to lost
3845 # ------------------------------------------------------------------
3847 my $ods = $self->editor->search_money_billing(
3849 xact => $self->circ->id,
3854 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3855 for my $bill (@$ods) {
3856 if( $U->is_true($bill->voided) ) {
3857 $logger->info("lost item returned - restoring overdue ".$bill->id);
3859 $bill->clear_void_time;
3860 $bill->voider($self->editor->requestor->id);
3861 my $note = ($bill->note) ? $bill->note . "\n" : '';
3862 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3864 $self->bail_on_events($self->editor->event)
3865 unless $self->editor->update_money_billing($bill);
3870 # ------------------------------------------------------------------
3871 # Lost-then-found item checked in. This sub generates new overdue
3872 # fines, beyond the point of any existing and possibly voided
3873 # overdue fines, up to the point of final checkin time (or max fine
3875 # ------------------------------------------------------------------
3876 sub generate_lost_overdue_fines {
3878 my $circ = $self->circ;
3879 my $e = $self->editor;
3881 # Re-open the transaction so the fine generator can see it
3882 if($circ->xact_finish or $circ->stop_fines) {
3884 $circ->clear_xact_finish;
3885 $circ->clear_stop_fines;
3886 $circ->clear_stop_fines_time;
3887 $e->update_action_circulation($circ) or return $e->die_event;
3891 $e->xact_begin; # generate_fines expects an in-xact editor
3892 $self->generate_fines;
3893 $circ = $self->circ; # generate fines re-fetches the circ
3897 # Re-close the transaction if no money is owed
3898 my ($obt) = $U->fetch_mbts($circ->id, $e);
3899 if ($obt and $obt->balance_owed == 0) {
3900 $circ->xact_finish('now');
3904 # Set stop fines if the fine generator didn't have to
3905 unless($circ->stop_fines) {
3906 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3907 $circ->stop_fines_time('now');
3911 # update the event data sent to the caller within the transaction
3912 $self->checkin_flesh_events;
3915 $e->update_action_circulation($circ) or return $e->die_event;