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;
1110 if ($self->checkout_is_for_hold) {
1111 # if this checkout will fulfill a hold, ignore CIRC blocks
1112 # and rely instead on the (later-checked) FULFILL block
1114 my @pen_names = grep {$_} map {$_->{fail_part}} @$results;
1115 my $fblock_pens = $self->editor->search_config_standing_penalty(
1116 {name => [@pen_names], block_list => {like => '%CIRC%'}});
1118 for my $res (@$results) {
1119 my $name = $res->{fail_part} || '';
1120 next if grep {$_->name eq $name} @$fblock_pens;
1121 push(@trimmed_results, $res);
1125 # not for hold or noncat
1126 @trimmed_results = @$results;
1130 # update the final set of test results
1131 $self->matrix_test_result(\@trimmed_results);
1133 push @allevents, $self->matrix_test_result_events;
1138 # ---------------------------------------------------------------------
1139 # # Now run the patron permit script
1140 # ---------------------------------------------------------------------
1141 $runner->load($self->circ_permit_patron);
1142 my $result = $runner->run or
1143 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1145 my $patron_events = $result->{events};
1147 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1148 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1149 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1150 $penalties = $penalties->{fatal_penalties};
1152 for my $pen (@$penalties) {
1153 # CIRC blocks are ignored if this is a FULFILL scenario
1154 next if $mask eq 'CIRC' and $self->checkout_is_for_hold;
1155 my $event = OpenILS::Event->new($pen->name);
1156 $event->{desc} = $pen->label;
1157 push(@allevents, $event);
1160 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1164 $_->{payload} = $self->copy if
1165 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1168 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1170 $self->push_events(@allevents);
1173 sub matrix_test_result_codes {
1175 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1178 sub matrix_test_result_events {
1181 my $event = new OpenILS::Event(
1182 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1184 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1186 } (@{$self->matrix_test_result});
1189 sub run_indb_circ_test {
1191 return $self->matrix_test_result if $self->matrix_test_result;
1193 my $dbfunc = ($self->is_renewal) ?
1194 'action.item_user_renew_test' : 'action.item_user_circ_test';
1196 if( $self->is_precat && $self->request_precat) {
1197 $self->make_precat_copy;
1198 return if $self->bail_out;
1201 my $results = $self->editor->json_query(
1205 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1211 $self->circ_test_success($U->is_true($results->[0]->{success}));
1213 if(my $mp = $results->[0]->{matchpoint}) {
1214 $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows});
1215 $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp));
1216 $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule}));
1217 if(defined($results->[0]->{renewals})) {
1218 $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
1220 $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
1221 if(defined($results->[0]->{grace_period})) {
1222 $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
1224 $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
1225 $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
1226 # Grab the *last* response for limit_groups, where it is more likely to be filled
1227 $self->limit_groups($results->[-1]->{limit_groups});
1230 return $self->matrix_test_result($results);
1233 # ---------------------------------------------------------------------
1234 # given a use and copy, this will calculate the circulation policy
1235 # parameters. Only works with in-db circ.
1236 # ---------------------------------------------------------------------
1240 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1242 $self->run_indb_circ_test;
1245 circ_test_success => $self->circ_test_success,
1246 failure_events => [],
1247 failure_codes => [],
1248 matchpoint => $self->circ_matrix_matchpoint
1251 unless($self->circ_test_success) {
1252 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1253 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1256 if($self->circ_matrix_matchpoint) {
1257 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1258 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1259 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1260 my $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1262 my $policy = $self->get_circ_policy(
1263 $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date);
1265 $$results{$_} = $$policy{$_} for keys %$policy;
1271 # ---------------------------------------------------------------------
1272 # Loads the circ policy info for duration, recurring fine, and max
1273 # fine based on the current copy
1274 # ---------------------------------------------------------------------
1275 sub get_circ_policy {
1276 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule, $hard_due_date) = @_;
1279 duration_rule => $duration_rule->name,
1280 recurring_fine_rule => $recurring_fine_rule->name,
1281 max_fine_rule => $max_fine_rule->name,
1282 max_fine => $self->get_max_fine_amount($max_fine_rule),
1283 fine_interval => $recurring_fine_rule->recurrence_interval,
1284 renewal_remaining => $duration_rule->max_renewals,
1285 grace_period => $recurring_fine_rule->grace_period
1288 if($hard_due_date) {
1289 $policy->{duration_date_ceiling} = $hard_due_date->ceiling_date;
1290 $policy->{duration_date_ceiling_force} = $hard_due_date->forceto;
1293 $policy->{duration_date_ceiling} = undef;
1294 $policy->{duration_date_ceiling_force} = undef;
1297 $policy->{duration} = $duration_rule->shrt
1298 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1299 $policy->{duration} = $duration_rule->normal
1300 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1301 $policy->{duration} = $duration_rule->extended
1302 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1304 $policy->{recurring_fine} = $recurring_fine_rule->low
1305 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1306 $policy->{recurring_fine} = $recurring_fine_rule->normal
1307 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1308 $policy->{recurring_fine} = $recurring_fine_rule->high
1309 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1314 sub get_max_fine_amount {
1316 my $max_fine_rule = shift;
1317 my $max_amount = $max_fine_rule->amount;
1319 # if is_percent is true then the max->amount is
1320 # use as a percentage of the copy price
1321 if ($U->is_true($max_fine_rule->is_percent)) {
1322 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1323 $max_amount = $price * $max_fine_rule->amount / 100;
1325 $U->ou_ancestor_setting_value(
1327 'circ.max_fine.cap_at_price',
1331 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1332 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1340 sub run_copy_permit_scripts {
1342 my $copy = $self->copy || return;
1343 my $runner = $self->script_runner;
1347 if(!$self->legacy_script_support) {
1348 my $results = $self->run_indb_circ_test;
1349 push @allevents, $self->matrix_test_result_events
1350 unless $self->circ_test_success;
1353 # ---------------------------------------------------------------------
1354 # Capture all of the copy permit events
1355 # ---------------------------------------------------------------------
1356 $runner->load($self->circ_permit_copy);
1357 my $result = $runner->run or
1358 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1359 my $copy_events = $result->{events};
1361 # ---------------------------------------------------------------------
1362 # Now collect all of the events together
1363 # ---------------------------------------------------------------------
1364 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1367 # See if this copy has an alert message
1368 my $ae = $self->check_copy_alert();
1369 push( @allevents, $ae ) if $ae;
1371 # uniquify the events
1372 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1373 @allevents = values %hash;
1375 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1377 $self->push_events(@allevents);
1381 sub check_copy_alert {
1383 return undef if $self->is_renewal;
1384 return OpenILS::Event->new(
1385 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1386 if $self->copy and $self->copy->alert_message;
1392 # --------------------------------------------------------------------------
1393 # If the call is overriding and has permissions to override every collected
1394 # event, the are cleared. Any event that the caller does not have
1395 # permission to override, will be left in the event list and bail_out will
1397 # XXX We need code in here to cancel any holds/transits on copies
1398 # that are being force-checked out
1399 # --------------------------------------------------------------------------
1400 sub override_events {
1402 my @events = @{$self->events};
1403 return unless @events;
1404 my $oargs = $self->override_args;
1406 if(!$self->override) {
1407 return $self->bail_out(1)
1408 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1413 for my $e (@events) {
1414 my $tc = $e->{textcode};
1415 next if $tc eq 'SUCCESS';
1416 if($oargs->{all} || grep { $_ eq $tc } @{$oargs->{events}}) {
1417 my $ov = "$tc.override";
1418 $logger->info("circulator: attempting to override event: $ov");
1420 return $self->bail_on_events($self->editor->event)
1421 unless( $self->editor->allowed($ov) );
1423 return $self->bail_out(1);
1429 # --------------------------------------------------------------------------
1430 # If there is an open claimsreturn circ on the requested copy, close the
1431 # circ if overriding, otherwise bail out
1432 # --------------------------------------------------------------------------
1433 sub handle_claims_returned {
1435 my $copy = $self->copy;
1437 my $CR = $self->editor->search_action_circulation(
1439 target_copy => $copy->id,
1440 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1441 checkin_time => undef,
1445 return unless ($CR = $CR->[0]);
1449 # - If the caller has set the override flag, we will check the item in
1450 if($self->override && ($self->override_args->{all} || grep { $_ eq 'CIRC_CLAIMS_RETURNED' } @{$self->override_args->{events}}) ) {
1452 $CR->checkin_time('now');
1453 $CR->checkin_scan_time('now');
1454 $CR->checkin_lib($self->circ_lib);
1455 $CR->checkin_workstation($self->editor->requestor->wsid);
1456 $CR->checkin_staff($self->editor->requestor->id);
1458 $evt = $self->editor->event
1459 unless $self->editor->update_action_circulation($CR);
1462 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1465 $self->bail_on_events($evt) if $evt;
1470 # --------------------------------------------------------------------------
1471 # This performs the checkout
1472 # --------------------------------------------------------------------------
1476 $self->log_me("do_checkout()");
1478 # make sure perms are good if this isn't a renewal
1479 unless( $self->is_renewal ) {
1480 return $self->bail_on_events($self->editor->event)
1481 unless( $self->editor->allowed('COPY_CHECKOUT') );
1484 # verify the permit key
1485 unless( $self->check_permit_key ) {
1486 if( $self->permit_override ) {
1487 return $self->bail_on_events($self->editor->event)
1488 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1490 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1494 # if this is a non-cataloged circ, build the circ and finish
1495 if( $self->is_noncat ) {
1496 $self->checkout_noncat;
1498 OpenILS::Event->new('SUCCESS',
1499 payload => { noncat_circ => $self->circ }));
1503 if( $self->is_precat ) {
1504 $self->make_precat_copy;
1505 return if $self->bail_out;
1507 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1508 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1511 $self->do_copy_checks;
1512 return if $self->bail_out;
1514 $self->run_checkout_scripts();
1515 return if $self->bail_out;
1517 $self->build_checkout_circ_object();
1518 return if $self->bail_out;
1520 my $modify_to_start = $self->booking_adjusted_due_date();
1521 return if $self->bail_out;
1523 $self->apply_modified_due_date($modify_to_start);
1524 return if $self->bail_out;
1526 return $self->bail_on_events($self->editor->event)
1527 unless $self->editor->create_action_circulation($self->circ);
1529 # refresh the circ to force local time zone for now
1530 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1532 if($self->limit_groups) {
1533 $self->editor->json_query({ from => ['action.link_circ_limit_groups', $self->circ->id, $self->limit_groups] });
1536 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1538 return if $self->bail_out;
1540 $self->apply_deposit_fee();
1541 return if $self->bail_out;
1543 $self->handle_checkout_holds();
1544 return if $self->bail_out;
1546 # ------------------------------------------------------------------------------
1547 # Update the patron penalty info in the DB. Run it for permit-overrides
1548 # since the penalties are not updated during the permit phase
1549 # ------------------------------------------------------------------------------
1550 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1552 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1555 if($self->is_renewal) {
1556 # flesh the billing summary for the checked-in circ
1557 $pcirc = $self->editor->retrieve_action_circulation([
1559 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1564 OpenILS::Event->new('SUCCESS',
1566 copy => $U->unflesh_copy($self->copy),
1567 volume => $self->volume,
1568 circ => $self->circ,
1570 holds_fulfilled => $self->fulfilled_holds,
1571 deposit_billing => $self->deposit_billing,
1572 rental_billing => $self->rental_billing,
1573 parent_circ => $pcirc,
1574 patron => ($self->return_patron) ? $self->patron : undef,
1575 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1581 sub apply_deposit_fee {
1583 my $copy = $self->copy;
1585 ($self->is_deposit and not $self->is_deposit_exempt) or
1586 ($self->is_rental and not $self->is_rental_exempt);
1588 return if $self->is_deposit and $self->skip_deposit_fee;
1589 return if $self->is_rental and $self->skip_rental_fee;
1591 my $bill = Fieldmapper::money::billing->new;
1592 my $amount = $copy->deposit_amount;
1596 if($self->is_deposit) {
1597 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1599 $self->deposit_billing($bill);
1601 $billing_type = OILS_BILLING_TYPE_RENTAL;
1603 $self->rental_billing($bill);
1606 $bill->xact($self->circ->id);
1607 $bill->amount($amount);
1608 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1609 $bill->billing_type($billing_type);
1610 $bill->btype($btype);
1611 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1613 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1618 my $copy = $self->copy;
1620 my $stat = $copy->status if ref $copy->status;
1621 my $loc = $copy->location if ref $copy->location;
1622 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1624 $copy->status($stat->id) if $stat;
1625 $copy->location($loc->id) if $loc;
1626 $copy->circ_lib($circ_lib->id) if $circ_lib;
1627 $copy->editor($self->editor->requestor->id);
1628 $copy->edit_date('now');
1629 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1631 return $self->bail_on_events($self->editor->event)
1632 unless $self->editor->update_asset_copy($self->copy);
1634 $copy->status($U->copy_status($copy->status));
1635 $copy->location($loc) if $loc;
1636 $copy->circ_lib($circ_lib) if $circ_lib;
1639 sub update_reservation {
1641 my $reservation = $self->reservation;
1643 my $usr = $reservation->usr;
1644 my $target_rt = $reservation->target_resource_type;
1645 my $target_r = $reservation->target_resource;
1646 my $current_r = $reservation->current_resource;
1648 $reservation->usr($usr->id) if ref $usr;
1649 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1650 $reservation->target_resource($target_r->id) if ref $target_r;
1651 $reservation->current_resource($current_r->id) if ref $current_r;
1653 return $self->bail_on_events($self->editor->event)
1654 unless $self->editor->update_booking_reservation($self->reservation);
1657 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1658 $self->reservation($reservation);
1662 sub bail_on_events {
1663 my( $self, @evts ) = @_;
1664 $self->push_events(@evts);
1668 # ------------------------------------------------------------------------------
1669 # A hold FULFILL block is just like a CIRC block, except that FULFILL only
1670 # affects copies that will fulfill holds and CIRC affects all other copies.
1671 # If blocks exists, bail, push Events onto the event pile, and return true.
1672 # ------------------------------------------------------------------------------
1673 sub check_hold_fulfill_blocks {
1676 # See if the user has any penalties applied that prevent hold fulfillment
1677 my $pens = $self->editor->json_query({
1678 select => {csp => ['name', 'label']},
1679 from => {ausp => {csp => {}}},
1682 usr => $self->patron->id,
1683 org_unit => $U->get_org_full_path($self->circ_lib),
1685 {stop_date => undef},
1686 {stop_date => {'>' => 'now'}}
1689 '+csp' => {block_list => {'like' => '%FULFILL%'}}
1693 return 0 unless @$pens;
1695 for my $pen (@$pens) {
1696 $logger->info("circulator: patron has hold FULFILL block " . $pen->{name});
1697 my $event = OpenILS::Event->new($pen->{name});
1698 $event->{desc} = $pen->{label};
1699 $self->push_events($event);
1702 $self->override_events;
1703 return $self->bail_out;
1707 # ------------------------------------------------------------------------------
1708 # When an item is checked out, see if we can fulfill a hold for this patron
1709 # ------------------------------------------------------------------------------
1710 sub handle_checkout_holds {
1712 my $copy = $self->copy;
1713 my $patron = $self->patron;
1715 my $e = $self->editor;
1716 $self->fulfilled_holds([]);
1718 # pre/non-cats can't fulfill a hold
1719 return if $self->is_precat or $self->is_noncat;
1721 my $hold = $e->search_action_hold_request({
1722 current_copy => $copy->id ,
1723 cancel_time => undef,
1724 fulfillment_time => undef,
1726 {expire_time => undef},
1727 {expire_time => {'>' => 'now'}}
1731 if($hold and $hold->usr != $patron->id) {
1732 # reset the hold since the copy is now checked out
1734 $logger->info("circulator: un-targeting hold ".$hold->id.
1735 " because copy ".$copy->id." is getting checked out");
1737 $hold->clear_prev_check_time;
1738 $hold->clear_current_copy;
1739 $hold->clear_capture_time;
1740 $hold->clear_shelf_time;
1741 $hold->clear_shelf_expire_time;
1742 $hold->clear_current_shelf_lib;
1744 return $self->bail_on_event($e->event)
1745 unless $e->update_action_hold_request($hold);
1751 $hold = $self->find_related_user_hold($copy, $patron) or return;
1752 $logger->info("circulator: found related hold to fulfill in checkout");
1755 return if $self->check_hold_fulfill_blocks;
1757 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1759 # if the hold was never officially captured, capture it.
1760 $hold->current_copy($copy->id);
1761 $hold->capture_time('now') unless $hold->capture_time;
1762 $hold->fulfillment_time('now');
1763 $hold->fulfillment_staff($e->requestor->id);
1764 $hold->fulfillment_lib($self->circ_lib);
1766 return $self->bail_on_events($e->event)
1767 unless $e->update_action_hold_request($hold);
1769 $holdcode->delete_hold_copy_maps($e, $hold->id);
1770 return $self->fulfilled_holds([$hold->id]);
1774 # ------------------------------------------------------------------------------
1775 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1776 # the patron directly targets the checked out item, see if there is another hold
1777 # for the patron that could be fulfilled by the checked out item. Fulfill the
1778 # oldest hold and only fulfill 1 of them.
1780 # For "another hold":
1782 # First, check for one that the copy matches via hold_copy_map, ensuring that
1783 # *any* hold type that this copy could fill may end up filled.
1785 # Then, if circ.checkout_fill_related_hold_exact_match_only is not enabled, look
1786 # for a Title (T) or Volume (V) hold that matches the item. This allows items
1787 # that are non-requestable to count as capturing those hold types.
1788 # ------------------------------------------------------------------------------
1789 sub find_related_user_hold {
1790 my($self, $copy, $patron) = @_;
1791 my $e = $self->editor;
1793 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1795 return undef unless $U->ou_ancestor_setting_value(
1796 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1798 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1800 select => {ahr => ['id']},
1809 fkey => 'current_copy',
1810 type => 'left' # there may be no current_copy
1817 fulfillment_time => undef,
1818 cancel_time => undef,
1820 {expire_time => undef},
1821 {expire_time => {'>' => 'now'}}
1825 target_copy => $self->copy->id
1829 {id => undef}, # left-join copy may be nonexistent
1830 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1834 order_by => {ahr => {request_time => {direction => 'asc'}}},
1838 my $hold_info = $e->json_query($args)->[0];
1839 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1840 return undef if $U->ou_ancestor_setting_value(
1841 $self->circ_lib, 'circ.checkout_fills_related_hold_exact_match_only', $e);
1843 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1845 select => {ahr => ['id']},
1850 fkey => 'current_copy',
1851 type => 'left' # there may be no current_copy
1858 fulfillment_time => undef,
1859 cancel_time => undef,
1861 {expire_time => undef},
1862 {expire_time => {'>' => 'now'}}
1869 target => $self->volume->id
1875 target => $self->title->id
1881 {id => undef}, # left-join copy may be nonexistent
1882 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1886 order_by => {ahr => {request_time => {direction => 'asc'}}},
1890 $hold_info = $e->json_query($args)->[0];
1891 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1896 sub run_checkout_scripts {
1901 my $runner = $self->script_runner;
1910 my $hard_due_date_name;
1912 if(!$self->legacy_script_support) {
1913 $self->run_indb_circ_test();
1914 $duration = $self->circ_matrix_matchpoint->duration_rule;
1915 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1916 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1917 $hard_due_date = $self->circ_matrix_matchpoint->hard_due_date;
1921 $runner->load($self->circ_duration);
1923 my $result = $runner->run or
1924 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1926 $duration_name = $result->{durationRule};
1927 $recurring_name = $result->{recurringFinesRule};
1928 $max_fine_name = $result->{maxFine};
1929 $hard_due_date_name = $result->{hardDueDate};
1932 $duration_name = $duration->name if $duration;
1933 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1936 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1937 return $self->bail_on_events($evt) if ($evt && !$nobail);
1939 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1940 return $self->bail_on_events($evt) if ($evt && !$nobail);
1942 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1943 return $self->bail_on_events($evt) if ($evt && !$nobail);
1945 if($hard_due_date_name) {
1946 ($hard_due_date, $evt) = $U->fetch_hard_due_date_by_name($hard_due_date_name);
1947 return $self->bail_on_events($evt) if ($evt && !$nobail);
1953 # The item circulates with an unlimited duration
1957 $hard_due_date = undef;
1960 $self->duration_rule($duration);
1961 $self->recurring_fines_rule($recurring);
1962 $self->max_fine_rule($max_fine);
1963 $self->hard_due_date($hard_due_date);
1967 sub build_checkout_circ_object {
1970 my $circ = Fieldmapper::action::circulation->new;
1971 my $duration = $self->duration_rule;
1972 my $max = $self->max_fine_rule;
1973 my $recurring = $self->recurring_fines_rule;
1974 my $hard_due_date = $self->hard_due_date;
1975 my $copy = $self->copy;
1976 my $patron = $self->patron;
1977 my $duration_date_ceiling;
1978 my $duration_date_ceiling_force;
1982 my $policy = $self->get_circ_policy($duration, $recurring, $max, $hard_due_date);
1983 $duration_date_ceiling = $policy->{duration_date_ceiling};
1984 $duration_date_ceiling_force = $policy->{duration_date_ceiling_force};
1986 my $dname = $duration->name;
1987 my $mname = $max->name;
1988 my $rname = $recurring->name;
1990 if($hard_due_date) {
1991 $hdname = $hard_due_date->name;
1994 $logger->debug("circulator: building circulation ".
1995 "with duration=$dname, maxfine=$mname, recurring=$rname, hard due date=$hdname");
1997 $circ->duration($policy->{duration});
1998 $circ->recurring_fine($policy->{recurring_fine});
1999 $circ->duration_rule($duration->name);
2000 $circ->recurring_fine_rule($recurring->name);
2001 $circ->max_fine_rule($max->name);
2002 $circ->max_fine($policy->{max_fine});
2003 $circ->fine_interval($recurring->recurrence_interval);
2004 $circ->renewal_remaining($duration->max_renewals);
2005 $circ->grace_period($policy->{grace_period});
2009 $logger->info("circulator: copy found with an unlimited circ duration");
2010 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
2011 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2012 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
2013 $circ->renewal_remaining(0);
2014 $circ->grace_period(0);
2017 $circ->target_copy( $copy->id );
2018 $circ->usr( $patron->id );
2019 $circ->circ_lib( $self->circ_lib );
2020 $circ->workstation($self->editor->requestor->wsid)
2021 if defined $self->editor->requestor->wsid;
2023 # renewals maintain a link to the parent circulation
2024 $circ->parent_circ($self->parent_circ);
2026 if( $self->is_renewal ) {
2027 $circ->opac_renewal('t') if $self->opac_renewal;
2028 $circ->phone_renewal('t') if $self->phone_renewal;
2029 $circ->desk_renewal('t') if $self->desk_renewal;
2030 $circ->renewal_remaining($self->renewal_remaining);
2031 $circ->circ_staff($self->editor->requestor->id);
2035 # if the user provided an overiding checkout time,
2036 # (e.g. the checkout really happened several hours ago), then
2037 # we apply that here. Does this need a perm??
2038 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
2039 if $self->checkout_time;
2041 # if a patron is renewing, 'requestor' will be the patron
2042 $circ->circ_staff($self->editor->requestor->id);
2043 $circ->due_date( $self->create_due_date($circ->duration, $duration_date_ceiling, $duration_date_ceiling_force) ) if $circ->duration;
2048 sub do_reservation_pickup {
2051 $self->log_me("do_reservation_pickup()");
2053 $self->reservation->pickup_time('now');
2056 $self->reservation->current_resource &&
2057 $U->is_true($self->reservation->target_resource_type->catalog_item)
2059 # We used to try to set $self->copy and $self->patron here,
2060 # but that should already be done.
2062 $self->run_checkout_scripts(1);
2064 my $duration = $self->duration_rule;
2065 my $max = $self->max_fine_rule;
2066 my $recurring = $self->recurring_fines_rule;
2068 if ($duration && $max && $recurring) {
2069 my $policy = $self->get_circ_policy($duration, $recurring, $max);
2071 my $dname = $duration->name;
2072 my $mname = $max->name;
2073 my $rname = $recurring->name;
2075 $logger->debug("circulator: updating reservation ".
2076 "with duration=$dname, maxfine=$mname, recurring=$rname");
2078 $self->reservation->fine_amount($policy->{recurring_fine});
2079 $self->reservation->max_fine($policy->{max_fine});
2080 $self->reservation->fine_interval($recurring->recurrence_interval);
2083 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
2084 $self->update_copy();
2087 $self->reservation->fine_amount(
2088 $self->reservation->target_resource_type->fine_amount
2090 $self->reservation->max_fine(
2091 $self->reservation->target_resource_type->max_fine
2093 $self->reservation->fine_interval(
2094 $self->reservation->target_resource_type->fine_interval
2098 $self->update_reservation();
2101 sub do_reservation_return {
2103 my $request = shift;
2105 $self->log_me("do_reservation_return()");
2107 if (not ref $self->reservation) {
2108 my ($reservation, $evt) =
2109 $U->fetch_booking_reservation($self->reservation);
2110 return $self->bail_on_events($evt) if $evt;
2111 $self->reservation($reservation);
2114 $self->generate_fines(1);
2115 $self->reservation->return_time('now');
2116 $self->update_reservation();
2117 $self->reshelve_copy if $self->copy;
2119 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
2120 $self->copy( $self->reservation->current_resource->catalog_item );
2124 sub booking_adjusted_due_date {
2126 my $circ = $self->circ;
2127 my $copy = $self->copy;
2129 return undef unless $self->use_booking;
2133 if( $self->due_date ) {
2135 return $self->bail_on_events($self->editor->event)
2136 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2138 $circ->due_date(cleanse_ISO8601($self->due_date));
2142 return unless $copy and $circ->due_date;
2145 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
2146 if (@$booking_items) {
2147 my $booking_item = $booking_items->[0];
2148 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
2150 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
2151 my $shorten_circ_setting = $resource_type->elbow_room ||
2152 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
2155 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
2156 my $bookings = $booking_ses->request(
2157 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
2158 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef, return_time => undef}}
2160 $booking_ses->disconnect;
2162 my $dt_parser = DateTime::Format::ISO8601->new;
2163 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
2165 for my $bid (@$bookings) {
2167 my $booking = $self->editor->retrieve_booking_reservation( $bid );
2169 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
2170 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
2172 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
2173 if ($booking_start < DateTime->now);
2176 if ($U->is_true($stop_circ_setting)) {
2177 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
2179 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
2180 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
2183 # We set the circ duration here only to affect the logic that will
2184 # later (in a DB trigger) mangle the time part of the due date to
2185 # 11:59pm. Having any circ duration that is not a whole number of
2186 # days is enough to prevent the "correction."
2187 my $new_circ_duration = $due_date->epoch - time;
2188 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
2189 $circ->duration("$new_circ_duration seconds");
2191 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
2195 return $self->bail_on_events($self->editor->event)
2196 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2202 sub apply_modified_due_date {
2204 my $shift_earlier = shift;
2205 my $circ = $self->circ;
2206 my $copy = $self->copy;
2208 if( $self->due_date ) {
2210 return $self->bail_on_events($self->editor->event)
2211 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
2213 $circ->due_date(cleanse_ISO8601($self->due_date));
2217 # if the due_date lands on a day when the location is closed
2218 return unless $copy and $circ->due_date;
2220 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
2222 # due-date overlap should be determined by the location the item
2223 # is checked out from, not the owning or circ lib of the item
2224 my $org = $self->circ_lib;
2226 $logger->info("circulator: circ searching for closed date overlap on lib $org".
2227 " with an item due date of ".$circ->due_date );
2229 my $dateinfo = $U->storagereq(
2230 'open-ils.storage.actor.org_unit.closed_date.overlap',
2231 $org, $circ->due_date );
2234 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2235 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2237 # XXX make the behavior more dynamic
2238 # for now, we just push the due date to after the close date
2239 if ($shift_earlier) {
2240 $circ->due_date($dateinfo->{start});
2242 $circ->due_date($dateinfo->{end});
2250 sub create_due_date {
2251 my( $self, $duration, $date_ceiling, $force_date ) = @_;
2253 # if there is a raw time component (e.g. from postgres),
2254 # turn it into an interval that interval_to_seconds can parse
2255 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2257 # for now, use the server timezone. TODO: use workstation org timezone
2258 my $due_date = DateTime->now(time_zone => 'local');
2260 # add the circ duration
2261 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2264 my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
2265 if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
2266 $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
2271 # return ISO8601 time with timezone
2272 return $due_date->strftime('%FT%T%z');
2277 sub make_precat_copy {
2279 my $copy = $self->copy;
2282 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2284 $copy->editor($self->editor->requestor->id);
2285 $copy->edit_date('now');
2286 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2287 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2288 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2289 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2290 $self->update_copy();
2294 $logger->info("circulator: Creating a new precataloged ".
2295 "copy in checkout with barcode " . $self->copy_barcode);
2297 $copy = Fieldmapper::asset::copy->new;
2298 $copy->circ_lib($self->circ_lib);
2299 $copy->creator($self->editor->requestor->id);
2300 $copy->editor($self->editor->requestor->id);
2301 $copy->barcode($self->copy_barcode);
2302 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2303 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2304 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2306 $copy->dummy_title($self->dummy_title || "");
2307 $copy->dummy_author($self->dummy_author || "");
2308 $copy->dummy_isbn($self->dummy_isbn || "");
2309 $copy->circ_modifier($self->circ_modifier);
2312 # See if we need to override the circ_lib for the copy with a configured circ_lib
2313 # Setting is shortname of the org unit
2314 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2315 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2317 if($precat_circ_lib) {
2318 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2321 $self->bail_on_events($self->editor->event);
2325 $copy->circ_lib($org->id);
2329 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2331 $self->push_events($self->editor->event);
2335 # this is a little bit of a hack, but we need to
2336 # get the copy into the script runner
2337 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2341 sub checkout_noncat {
2347 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2348 my $count = $self->noncat_count || 1;
2349 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2351 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2355 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2356 $self->editor->requestor->id,
2364 $self->push_events($evt);
2372 # If a copy goes into transit and is then checked in before the transit checkin
2373 # interval has expired, push an event onto the overridable events list.
2374 sub check_transit_checkin_interval {
2377 # only concerned with in-transit items
2378 return unless $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT;
2380 # no interval, no problem
2381 my $interval = $U->ou_ancestor_setting_value($self->circ_lib, 'circ.transit.min_checkin_interval');
2382 return unless $interval;
2384 # capture the transit so we don't have to fetch it again later during checkin
2386 $self->editor->search_action_transit_copy(
2387 {target_copy => $self->copy->id, dest_recv_time => undef}
2391 # transit from X to X for whatever reason has no min interval
2392 return if $self->transit->source == $self->transit->dest;
2394 my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
2395 my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
2396 my $horizon = $t_start->add(seconds => $seconds);
2398 # See if we are still within the transit checkin forbidden range
2399 $self->push_events(OpenILS::Event->new('TRANSIT_CHECKIN_INTERVAL_BLOCK'))
2400 if $horizon > DateTime->now;
2403 # Retarget local holds at checkin
2404 sub checkin_retarget {
2406 return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
2407 return unless $self->is_checkin; # Renewals need not be checked
2408 return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
2409 return if $self->is_precat; # No holds for precats
2410 return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
2411 return unless $U->is_true($self->copy->holdable); # Not holdable, shouldn't capture holds.
2412 my $status = $U->copy_status($self->copy->status);
2413 return unless $U->is_true($status->holdable); # Current status not holdable means no hold will ever target the item
2414 # Specifically target items that are likely new (by status ID)
2415 return unless $status->id == OILS_COPY_STATUS_IN_PROCESS || $self->retarget_mode =~ m/\.all/;
2416 my $location = $self->copy->location;
2417 if(!ref($location)) {
2418 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2419 $self->copy->location($location);
2421 return unless $U->is_true($location->holdable); # Don't bother on non-holdable locations
2423 # Fetch holds for the bib
2424 my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
2425 $self->editor->authtoken,
2428 capture_time => undef, # No touching captured holds
2429 frozen => 'f', # Don't bother with frozen holds
2430 pickup_lib => $self->circ_lib # Only holds actually here
2433 # Error? Skip the step.
2434 return if exists $result->{"ilsevent"};
2438 foreach my $holdlist (keys %{$result}) {
2439 push @$holds, @{$result->{$holdlist}};
2442 return if scalar(@$holds) == 0; # No holds, no retargeting
2444 # Check for parts on this copy
2445 my $parts = $self->editor->search_asset_copy_part_map({ target_copy => $self->copy->id });
2446 my %parts_hash = ();
2447 %parts_hash = map {$_->part, 1} @$parts if @$parts;
2449 # Loop over holds in request-ish order
2450 # Stage 1: Get them into request-ish order
2451 # Also grab type and target for skipping low hanging ones
2452 $result = $self->editor->json_query({
2453 "select" => { "ahr" => ["id", "hold_type", "target"] },
2454 "from" => { "ahr" => { "au" => { "fkey" => "usr", "join" => "pgt"} } },
2455 "where" => { "id" => $holds },
2457 { "class" => "pgt", "field" => "hold_priority"},
2458 { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
2459 { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
2460 { "class" => "ahr", "field" => "request_time"}
2465 if (ref $result eq "ARRAY" and scalar @$result) {
2466 foreach (@{$result}) {
2467 # Copy level, but not this copy?
2468 next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
2469 and $_->{target} != $self->copy->id);
2470 # Volume level, but not this volume?
2471 next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
2472 if(@$parts) { # We have parts?
2474 next if ($_->{hold_type} eq 'T');
2475 # Skip part holds for parts not on this copy
2476 next if ($_->{hold_type} eq 'P' and not $parts_hash{$_->{target}});
2478 # No parts, no part holds
2479 next if ($_->{hold_type} eq 'P');
2481 # So much for easy stuff, attempt a retarget!
2482 my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
2483 if(ref $tresult eq "ARRAY" and scalar @$tresult) {
2484 last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
2492 $self->log_me("do_checkin()");
2494 return $self->bail_on_events(
2495 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2498 $self->check_transit_checkin_interval;
2499 $self->checkin_retarget;
2501 # the renew code and mk_env should have already found our circulation object
2502 unless( $self->circ ) {
2504 my $circs = $self->editor->search_action_circulation(
2505 { target_copy => $self->copy->id, checkin_time => undef });
2507 $self->circ($$circs[0]);
2509 # for now, just warn if there are multiple open circs on a copy
2510 $logger->warn("circulator: we have ".scalar(@$circs).
2511 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2514 # run the fine generator against this circ, if this circ is there
2515 $self->generate_fines_start if $self->circ;
2517 if( $self->checkin_check_holds_shelf() ) {
2518 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2519 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2520 if($self->fake_hold_dest) {
2521 $self->hold->pickup_lib($self->circ_lib);
2523 $self->checkin_flesh_events;
2527 unless( $self->is_renewal ) {
2528 return $self->bail_on_events($self->editor->event)
2529 unless $self->editor->allowed('COPY_CHECKIN');
2532 $self->push_events($self->check_copy_alert());
2533 $self->push_events($self->check_checkin_copy_status());
2535 # if the circ is marked as 'claims returned', add the event to the list
2536 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2537 if ($self->circ and $self->circ->stop_fines
2538 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2540 $self->check_circ_deposit();
2542 # handle the overridable events
2543 $self->override_events unless $self->is_renewal;
2544 return if $self->bail_out;
2546 if( $self->copy and !$self->transit ) {
2548 $self->editor->search_action_transit_copy(
2549 { target_copy => $self->copy->id, dest_recv_time => undef }
2555 $self->generate_fines_finish;
2556 $self->checkin_handle_circ;
2557 return if $self->bail_out;
2558 $self->checkin_changed(1);
2560 } elsif( $self->transit ) {
2561 my $hold_transit = $self->process_received_transit;
2562 $self->checkin_changed(1);
2564 if( $self->bail_out ) {
2565 $self->checkin_flesh_events;
2569 if( my $e = $self->check_checkin_copy_status() ) {
2570 # If the original copy status is special, alert the caller
2571 my $ev = $self->events;
2572 $self->events([$e]);
2573 $self->override_events;
2574 return if $self->bail_out;
2578 if( $hold_transit or
2579 $U->copy_status($self->copy->status)->id
2580 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2583 if( $hold_transit ) {
2584 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2586 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2591 if( $hold and ( $hold->cancel_time or $hold->fulfillment_time ) ) { # this transited hold was cancelled or filled mid-transit
2593 $logger->info("circulator: we received a transit on a cancelled or filled hold " . $hold->id);
2594 $self->reshelve_copy(1);
2595 $self->cancelled_hold_transit(1);
2596 $self->notify_hold(0); # don't notify for cancelled holds
2597 $self->fake_hold_dest(0);
2598 return if $self->bail_out;
2600 } elsif ($hold and $hold->hold_type eq 'R') {
2602 $self->copy->status(OILS_COPY_STATUS_CATALOGING);
2603 $self->notify_hold(0); # No need to notify
2604 $self->fake_hold_dest(0);
2605 $self->noop(1); # Don't try and capture for other holds/transits now
2606 $self->update_copy();
2607 $hold->fulfillment_time('now');
2608 $self->bail_on_events($self->editor->event)
2609 unless $self->editor->update_action_hold_request($hold);
2613 # hold transited to correct location
2614 if($self->fake_hold_dest) {
2615 $hold->pickup_lib($self->circ_lib);
2617 $self->checkin_flesh_events;
2622 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2624 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2625 " that is in-transit, but there is no transit.. repairing");
2626 $self->reshelve_copy(1);
2627 return if $self->bail_out;
2630 if( $self->is_renewal ) {
2631 $self->finish_fines_and_voiding;
2632 return if $self->bail_out;
2633 $self->push_events(OpenILS::Event->new('SUCCESS'));
2637 # ------------------------------------------------------------------------------
2638 # Circulations and transits are now closed where necessary. Now go on to see if
2639 # this copy can fulfill a hold or needs to be routed to a different location
2640 # ------------------------------------------------------------------------------
2642 my $needed_for_something = 0; # formerly "needed_for_hold"
2644 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2646 if (!$self->remote_hold) {
2647 if ($self->use_booking) {
2648 my $potential_hold = $self->hold_capture_is_possible;
2649 my $potential_reservation = $self->reservation_capture_is_possible;
2651 if ($potential_hold and $potential_reservation) {
2652 $logger->info("circulator: item could fulfill either hold or reservation");
2653 $self->push_events(new OpenILS::Event(
2654 "HOLD_RESERVATION_CONFLICT",
2655 "hold" => $potential_hold,
2656 "reservation" => $potential_reservation
2658 return if $self->bail_out;
2659 } elsif ($potential_hold) {
2660 $needed_for_something =
2661 $self->attempt_checkin_hold_capture;
2662 } elsif ($potential_reservation) {
2663 $needed_for_something =
2664 $self->attempt_checkin_reservation_capture;
2667 $needed_for_something = $self->attempt_checkin_hold_capture;
2670 return if $self->bail_out;
2672 unless($needed_for_something) {
2673 my $circ_lib = (ref $self->copy->circ_lib) ?
2674 $self->copy->circ_lib->id : $self->copy->circ_lib;
2676 if( $self->remote_hold ) {
2677 $circ_lib = $self->remote_hold->pickup_lib;
2678 $logger->warn("circulator: Copy ".$self->copy->barcode.
2679 " is on a remote hold's shelf, sending to $circ_lib");
2682 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2684 my $suppress_transit = 0;
2686 if( $circ_lib != $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) {
2687 my $suppress_transit_source = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_non_hold');
2688 if($suppress_transit_source && $suppress_transit_source->{value}) {
2689 my $suppress_transit_dest = $U->ou_ancestor_setting($circ_lib, 'circ.transit.suppress_non_hold');
2690 if($suppress_transit_dest && $suppress_transit_source->{value} eq $suppress_transit_dest->{value}) {
2691 $logger->info("circulator: copy is within transit suppress group: ".$self->copy->barcode." ".$suppress_transit_source->{value});
2692 $suppress_transit = 1;
2697 if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
2698 # copy is where it needs to be, either for hold or reshelving
2700 $self->checkin_handle_precat();
2701 return if $self->bail_out;
2704 # copy needs to transit "home", or stick here if it's a floating copy
2706 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2707 $self->checkin_changed(1);
2708 $self->copy->circ_lib( $self->circ_lib );
2711 my $bc = $self->copy->barcode;
2712 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2713 $self->checkin_build_copy_transit($circ_lib);
2714 return if $self->bail_out;
2715 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2719 } else { # no-op checkin
2720 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2721 $self->checkin_changed(1);
2722 $self->copy->circ_lib( $self->circ_lib );
2727 if($self->claims_never_checked_out and
2728 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2730 # the item was not supposed to be checked out to the user and should now be marked as missing
2731 $self->copy->status(OILS_COPY_STATUS_MISSING);
2735 $self->reshelve_copy unless $needed_for_something;
2738 return if $self->bail_out;
2740 unless($self->checkin_changed) {
2742 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2743 my $stat = $U->copy_status($self->copy->status)->id;
2745 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2746 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2747 $self->bail_out(1); # no need to commit anything
2751 $self->push_events(OpenILS::Event->new('SUCCESS'))
2752 unless @{$self->events};
2755 $self->finish_fines_and_voiding;
2757 OpenILS::Utils::Penalty->calculate_penalties(
2758 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2760 $self->checkin_flesh_events;
2764 sub finish_fines_and_voiding {
2766 return unless $self->circ;
2768 # gather any updates to the circ after fine generation, if there was a circ
2769 $self->generate_fines_finish;
2771 return unless $self->backdate or $self->void_overdues;
2773 # void overdues after fine generation to prevent concurrent DB access to overdue billings
2774 my $note = 'System: Amnesty Checkin' if $self->void_overdues;
2776 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2777 $self->editor, $self->circ, $self->backdate, $note);
2779 return $self->bail_on_events($evt) if $evt;
2781 # make sure the circ isn't closed if we just voided some fines
2782 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $self->circ->id);
2783 return $self->bail_on_events($evt) if $evt;
2789 # if a deposit was payed for this item, push the event
2790 sub check_circ_deposit {
2792 return unless $self->circ;
2793 my $deposit = $self->editor->search_money_billing(
2795 xact => $self->circ->id,
2797 }, {idlist => 1})->[0];
2799 $self->push_events(OpenILS::Event->new(
2800 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2805 my $force = $self->force || shift;
2806 my $copy = $self->copy;
2808 my $stat = $U->copy_status($copy->status)->id;
2811 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2812 $stat != OILS_COPY_STATUS_CATALOGING and
2813 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2814 $stat != OILS_COPY_STATUS_RESHELVING )) {
2816 $copy->status( OILS_COPY_STATUS_RESHELVING );
2818 $self->checkin_changed(1);
2823 # Returns true if the item is at the current location
2824 # because it was transited there for a hold and the
2825 # hold has not been fulfilled
2826 sub checkin_check_holds_shelf {
2828 return 0 unless $self->copy;
2831 $U->copy_status($self->copy->status)->id ==
2832 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2834 # Attempt to clear shelf expired holds for this copy
2835 $holdcode->method_lookup('open-ils.circ.hold.clear_shelf.process')->run($self->editor->authtoken, $self->circ_lib, $self->copy->id)
2836 if($self->clear_expired);
2838 # find the hold that put us on the holds shelf
2839 my $holds = $self->editor->search_action_hold_request(
2841 current_copy => $self->copy->id,
2842 capture_time => { '!=' => undef },
2843 fulfillment_time => undef,
2844 cancel_time => undef,
2849 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2850 $self->reshelve_copy(1);
2854 my $hold = $$holds[0];
2856 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2857 $hold->id. "] for copy ".$self->copy->barcode);
2859 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
2860 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
2861 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
2862 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
2863 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
2864 $logger->info("circulator: hold is within hold transit suppress group .. we're done: ".$self->copy->barcode." ".$suppress_transit_circ->{value});
2865 $self->fake_hold_dest(1);
2871 if( $hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit ) {
2872 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2876 $logger->info("circulator: hold is not for here..");
2877 $self->remote_hold($hold);
2882 sub checkin_handle_precat {
2884 my $copy = $self->copy;
2886 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2887 $copy->status(OILS_COPY_STATUS_CATALOGING);
2888 $self->update_copy();
2889 $self->checkin_changed(1);
2890 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2895 sub checkin_build_copy_transit {
2898 my $copy = $self->copy;
2899 my $transit = Fieldmapper::action::transit_copy->new;
2901 # if we are transiting an item to the shelf shelf, it's a hold transit
2902 if (my $hold = $self->remote_hold) {
2903 $transit = Fieldmapper::action::hold_transit_copy->new;
2904 $transit->hold($hold->id);
2906 # the item is going into transit, remove any shelf-iness
2907 if ($hold->current_shelf_lib or $hold->shelf_time) {
2908 $hold->clear_current_shelf_lib;
2909 $hold->clear_shelf_time;
2910 return $self->bail_on_events($self->editor->event)
2911 unless $self->editor->update_action_hold_request($hold);
2915 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2916 $logger->info("circulator: transiting copy to $dest");
2918 $transit->source($self->circ_lib);
2919 $transit->dest($dest);
2920 $transit->target_copy($copy->id);
2921 $transit->source_send_time('now');
2922 $transit->copy_status( $U->copy_status($copy->status)->id );
2924 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2926 if ($self->remote_hold) {
2927 return $self->bail_on_events($self->editor->event)
2928 unless $self->editor->create_action_hold_transit_copy($transit);
2930 return $self->bail_on_events($self->editor->event)
2931 unless $self->editor->create_action_transit_copy($transit);
2934 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2936 $self->checkin_changed(1);
2940 sub hold_capture_is_possible {
2942 my $copy = $self->copy;
2944 # we've been explicitly told not to capture any holds
2945 return 0 if $self->capture eq 'nocapture';
2947 # See if this copy can fulfill any holds
2948 my $hold = $holdcode->find_nearest_permitted_hold(
2949 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2951 return undef if ref $hold eq "HASH" and
2952 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2956 sub reservation_capture_is_possible {
2958 my $copy = $self->copy;
2960 # we've been explicitly told not to capture any holds
2961 return 0 if $self->capture eq 'nocapture';
2963 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2964 my $resv = $booking_ses->request(
2965 "open-ils.booking.reservations.could_capture",
2966 $self->editor->authtoken, $copy->barcode
2968 $booking_ses->disconnect;
2969 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2970 $self->push_events($resv);
2976 # returns true if the item was used (or may potentially be used
2977 # in subsequent calls) to capture a hold.
2978 sub attempt_checkin_hold_capture {
2980 my $copy = $self->copy;
2982 # we've been explicitly told not to capture any holds
2983 return 0 if $self->capture eq 'nocapture';
2985 # See if this copy can fulfill any holds
2986 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2987 $self->editor, $copy, $self->editor->requestor );
2990 $logger->debug("circulator: no potential permitted".
2991 "holds found for copy ".$copy->barcode);
2995 if($self->capture ne 'capture') {
2996 # see if this item is in a hold-capture-delay location
2997 my $location = $self->copy->location;
2998 if(!ref($location)) {
2999 $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
3000 $self->copy->location($location);
3002 if($U->is_true($location->hold_verify)) {
3003 $self->bail_on_events(
3004 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
3009 $self->retarget($retarget);
3011 my $suppress_transit = 0;
3012 if( $hold->pickup_lib != $self->circ_lib and not $self->hold_as_transit ) {
3013 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, 'circ.transit.suppress_hold');
3014 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3015 my $suppress_transit_pickup = $U->ou_ancestor_setting($hold->pickup_lib, 'circ.transit.suppress_hold');
3016 if($suppress_transit_pickup && $suppress_transit_circ->{value} eq $suppress_transit_pickup->{value}) {
3017 $suppress_transit = 1;
3018 $self->hold->pickup_lib($self->circ_lib);
3023 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
3025 $hold->current_copy($copy->id);
3026 $hold->capture_time('now');
3027 $self->put_hold_on_shelf($hold)
3028 if ($suppress_transit || ($hold->pickup_lib == $self->circ_lib and not $self->hold_as_transit) );
3030 # prevent DB errors caused by fetching
3031 # holds from storage, and updating through cstore
3032 $hold->clear_fulfillment_time;
3033 $hold->clear_fulfillment_staff;
3034 $hold->clear_fulfillment_lib;
3035 $hold->clear_expire_time;
3036 $hold->clear_cancel_time;
3037 $hold->clear_prev_check_time unless $hold->prev_check_time;
3039 $self->bail_on_events($self->editor->event)
3040 unless $self->editor->update_action_hold_request($hold);
3042 $self->checkin_changed(1);
3044 return 0 if $self->bail_out;
3046 if( $suppress_transit or ( $hold->pickup_lib == $self->circ_lib && not $self->hold_as_transit ) ) {
3048 if ($hold->hold_type eq 'R') {
3049 $copy->status(OILS_COPY_STATUS_CATALOGING);
3050 $hold->fulfillment_time('now');
3051 $self->noop(1); # Block other transit/hold checks
3052 $self->bail_on_events($self->editor->event)
3053 unless $self->editor->update_action_hold_request($hold);
3055 # This hold was captured in the correct location
3056 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3057 $self->push_events(OpenILS::Event->new('SUCCESS'));
3059 #$self->do_hold_notify($hold->id);
3060 $self->notify_hold($hold->id);
3065 # Hold needs to be picked up elsewhere. Build a hold
3066 # transit and route the item.
3067 $self->checkin_build_hold_transit();
3068 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
3069 return 0 if $self->bail_out;
3070 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
3073 # make sure we save the copy status
3075 return 0 if $copy->status == OILS_COPY_STATUS_CATALOGING;
3079 sub attempt_checkin_reservation_capture {
3081 my $copy = $self->copy;
3083 # we've been explicitly told not to capture any holds
3084 return 0 if $self->capture eq 'nocapture';
3086 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
3087 my $evt = $booking_ses->request(
3088 "open-ils.booking.resources.capture_for_reservation",
3089 $self->editor->authtoken,
3091 1 # don't update copy - we probably have it locked
3093 $booking_ses->disconnect;
3095 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
3097 "open-ils.booking.resources.capture_for_reservation " .
3098 "didn't return an event!"
3102 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
3103 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
3105 # not-transferable is an error event we'll pass on the user
3106 $logger->warn("reservation capture attempted against non-transferable item");
3107 $self->push_events($evt);
3109 } elsif ($evt->{"textcode"} eq "SUCCESS") {
3110 # Re-retrieve copy as reservation capture may have changed
3111 # its status and whatnot.
3113 "circulator: booking capture win on copy " . $self->copy->id
3115 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
3117 "circulator: changing copy " . $self->copy->id .
3118 "'s status from " . $self->copy->status . " to " .
3121 $self->copy->status($new_copy_status);
3124 $self->reservation($evt->{"payload"}->{"reservation"});
3126 if (exists $evt->{"payload"}->{"transit"}) {
3130 "org" => $evt->{"payload"}->{"transit"}->dest
3134 $self->checkin_changed(1);
3138 # other results are treated as "nothing to capture"
3142 sub do_hold_notify {
3143 my( $self, $holdid ) = @_;
3145 my $e = new_editor(xact => 1);
3146 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
3148 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3149 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
3151 $logger->info("circulator: running delayed hold notify process");
3153 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3154 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
3156 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
3157 hold_id => $holdid, requestor => $self->editor->requestor);
3159 $logger->debug("circulator: built hold notifier");
3161 if(!$notifier->event) {
3163 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
3165 my $stat = $notifier->send_email_notify;
3166 if( $stat == '1' ) {
3167 $logger->info("circulator: hold notify succeeded for hold $holdid");
3171 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
3174 $logger->info("circulator: Not sending hold notification since the patron has no email address");
3178 sub retarget_holds {
3180 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
3181 my $ses = OpenSRF::AppSession->create('open-ils.storage');
3182 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
3183 # no reason to wait for the return value
3187 sub checkin_build_hold_transit {
3190 my $copy = $self->copy;
3191 my $hold = $self->hold;
3192 my $trans = Fieldmapper::action::hold_transit_copy->new;
3194 $logger->debug("circulator: building hold transit for ".$copy->barcode);
3196 $trans->hold($hold->id);
3197 $trans->source($self->circ_lib);
3198 $trans->dest($hold->pickup_lib);
3199 $trans->source_send_time("now");
3200 $trans->target_copy($copy->id);
3202 # when the copy gets to its destination, it will recover
3203 # this status - put it onto the holds shelf
3204 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
3206 return $self->bail_on_events($self->editor->event)
3207 unless $self->editor->create_action_hold_transit_copy($trans);
3212 sub process_received_transit {
3214 my $copy = $self->copy;
3215 my $copyid = $self->copy->id;
3217 my $status_name = $U->copy_status($copy->status)->name;
3218 $logger->debug("circulator: attempting transit receive on ".
3219 "copy $copyid. Copy status is $status_name");
3221 my $transit = $self->transit;
3223 # Check if we are in a transit suppress range
3224 my $suppress_transit = 0;
3225 if ( $transit->dest != $self->circ_lib and not ( $self->hold_as_transit and $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) ) {
3226 my $suppress_setting = ($transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF ? 'circ.transit.suppress_hold' : 'circ.transit.suppress_non_hold');
3227 my $suppress_transit_circ = $U->ou_ancestor_setting($self->circ_lib, $suppress_setting);
3228 if($suppress_transit_circ && $suppress_transit_circ->{value}) {
3229 my $suppress_transit_dest = $U->ou_ancestor_setting($transit->dest, $suppress_setting);
3230 if($suppress_transit_dest && $suppress_transit_dest->{value} eq $suppress_transit_circ->{value}) {
3231 $suppress_transit = 1;
3232 $self->fake_hold_dest(1) if $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
3236 if( not $suppress_transit and ( $transit->dest != $self->circ_lib or ($self->hold_as_transit && $transit->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF) ) ) {
3237 # - this item is in-transit to a different location
3238 # - Or we are capturing holds as transits, so why create a new transit?
3240 my $tid = $transit->id;
3241 my $loc = $self->circ_lib;
3242 my $dest = $transit->dest;
3244 $logger->info("circulator: Fowarding transit on copy which is destined ".
3245 "for a different location. transit=$tid, copy=$copyid, current ".
3246 "location=$loc, destination location=$dest");
3248 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
3250 # grab the associated hold object if available
3251 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
3252 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
3254 return $self->bail_on_events($evt);
3257 # The transit is received, set the receive time
3258 $transit->dest_recv_time('now');
3259 $self->bail_on_events($self->editor->event)
3260 unless $self->editor->update_action_transit_copy($transit);
3262 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
3264 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
3265 $copy->status( $transit->copy_status );
3266 $self->update_copy();
3267 return if $self->bail_out;
3271 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
3273 # hold has arrived at destination, set shelf time
3274 $self->put_hold_on_shelf($hold);
3275 $self->bail_on_events($self->editor->event)
3276 unless $self->editor->update_action_hold_request($hold);
3277 return if $self->bail_out;
3279 $self->notify_hold($hold_transit->hold);
3284 OpenILS::Event->new(
3287 payload => { transit => $transit, holdtransit => $hold_transit } ));
3289 return $hold_transit;
3293 # ------------------------------------------------------------------
3294 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
3295 # ------------------------------------------------------------------
3296 sub put_hold_on_shelf {
3297 my($self, $hold) = @_;
3298 $hold->shelf_time('now');
3299 $hold->current_shelf_lib($self->circ_lib);
3300 $holdcode->set_hold_shelf_expire_time($hold, $self->editor);
3306 sub generate_fines {
3308 my $reservation = shift;
3310 $self->generate_fines_start($reservation);
3311 $self->generate_fines_finish($reservation);
3316 sub generate_fines_start {
3318 my $reservation = shift;
3319 my $dt_parser = DateTime::Format::ISO8601->new;
3321 my $obj = $reservation ? $self->reservation : $self->circ;
3323 # If we have a grace period
3324 if($obj->can('grace_period')) {
3325 # Parse out the due date
3326 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
3327 # Add the grace period to the due date
3328 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
3329 # Don't generate fines on circs still in grace period
3330 return undef if ($due_date > DateTime->now);
3333 if (!exists($self->{_gen_fines_req})) {
3334 $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage')
3336 'open-ils.storage.action.circulation.overdue.generate_fines',
3344 sub generate_fines_finish {
3346 my $reservation = shift;
3348 return undef unless $self->{_gen_fines_req};
3350 my $id = $reservation ? $self->reservation->id : $self->circ->id;
3352 $self->{_gen_fines_req}->wait_complete;
3353 delete($self->{_gen_fines_req});
3355 # refresh the circ in case the fine generator set the stop_fines field
3356 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
3357 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
3362 sub checkin_handle_circ {
3364 my $circ = $self->circ;
3365 my $copy = $self->copy;
3369 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
3371 # backdate the circ if necessary
3372 if($self->backdate) {
3373 my $evt = $self->checkin_handle_backdate;
3374 return $self->bail_on_events($evt) if $evt;
3377 if(!$circ->stop_fines) {
3378 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3379 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
3380 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
3381 $circ->stop_fines_time('now');
3382 $circ->stop_fines_time($self->backdate) if $self->backdate;
3385 # Set the checkin vars since we have the item
3386 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
3388 # capture the true scan time for back-dated checkins
3389 $circ->checkin_scan_time('now');
3391 $circ->checkin_staff($self->editor->requestor->id);
3392 $circ->checkin_lib($self->circ_lib);
3393 $circ->checkin_workstation($self->editor->requestor->wsid);
3395 my $circ_lib = (ref $self->copy->circ_lib) ?
3396 $self->copy->circ_lib->id : $self->copy->circ_lib;
3397 my $stat = $U->copy_status($self->copy->status)->id;
3399 if ($stat == OILS_COPY_STATUS_LOST) {
3400 # we will now handle lost fines, but the copy will retain its 'lost'
3401 # status if it needs to transit home unless lost_immediately_available
3404 # if we decide to also delay fine handling until the item arrives home,
3405 # we will need to call lost fine handling code both when checking items
3406 # in and also when receiving transits
3407 $self->checkin_handle_lost($circ_lib);
3408 } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
3409 $logger->info("circulator: not updating copy status on checkin because copy is missing");
3411 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3416 # see if there are any fines owed on this circ. if not, close it
3417 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
3418 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
3420 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
3422 return $self->bail_on_events($self->editor->event)
3423 unless $self->editor->update_action_circulation($circ);
3429 # ------------------------------------------------------------------
3430 # See if we need to void billings for lost checkin
3431 # ------------------------------------------------------------------
3432 sub checkin_handle_lost {
3434 my $circ_lib = shift;
3435 my $circ = $self->circ;
3437 my $max_return = $U->ou_ancestor_setting_value(
3438 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
3443 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
3444 $tm[5] -= 1 if $tm[5] > 0;
3445 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
3447 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
3448 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
3450 $max_return = 0 if $today < $last_chance;
3453 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
3455 my $void_lost = $U->ou_ancestor_setting_value(
3456 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
3457 my $void_lost_fee = $U->ou_ancestor_setting_value(
3458 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
3459 my $restore_od = $U->ou_ancestor_setting_value(
3460 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
3461 $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
3462 $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
3464 $self->checkin_handle_lost_now_found(3) if $void_lost;
3465 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
3466 $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
3469 if ($circ_lib != $self->circ_lib) {
3470 # if the item is not home, check to see if we want to retain the lost
3471 # status at this point in the process
3472 my $immediately_available = $U->ou_ancestor_setting_value($circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
3474 if ($immediately_available) {
3475 # lost item status does not need to be retained, so give it a
3476 # reshelving status as if it were a normal checkin
3477 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3480 $logger->info("circulator: not updating copy status on checkin because copy is lost");
3483 # lost item is home and processed, treat like a normal checkin from
3485 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
3491 sub checkin_handle_backdate {
3494 # ------------------------------------------------------------------
3495 # clean up the backdate for date comparison
3496 # XXX We are currently taking the due-time from the original due-date,
3497 # not the input. Do we need to do this? This certainly interferes with
3498 # backdating of hourly checkouts, but that is likely a very rare case.
3499 # ------------------------------------------------------------------
3500 my $bd = cleanse_ISO8601($self->backdate);
3501 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
3502 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
3503 $new_date->set_hour($original_date->hour());
3504 $new_date->set_minute($original_date->minute());
3505 $bd = cleanse_ISO8601($new_date->datetime());
3507 $self->backdate($bd);
3512 sub check_checkin_copy_status {
3514 my $copy = $self->copy;
3516 my $status = $U->copy_status($copy->status)->id;
3519 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3520 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3521 $status == OILS_COPY_STATUS_IN_PROCESS ||
3522 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3523 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3524 $status == OILS_COPY_STATUS_CATALOGING ||
3525 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3526 $status == OILS_COPY_STATUS_RESHELVING );
3528 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3529 if( $status == OILS_COPY_STATUS_LOST );
3531 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3532 if( $status == OILS_COPY_STATUS_MISSING );
3534 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3539 # --------------------------------------------------------------------------
3540 # On checkin, we need to return as many relevant objects as we can
3541 # --------------------------------------------------------------------------
3542 sub checkin_flesh_events {
3545 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3546 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3547 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3550 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3553 if($self->hold and !$self->hold->cancel_time) {
3554 $hold = $self->hold;
3555 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3559 # if we checked in a circulation, flesh the billing summary data
3560 $self->circ->billable_transaction(
3561 $self->editor->retrieve_money_billable_transaction([
3563 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3569 # flesh some patron fields before returning
3571 $self->editor->retrieve_actor_user([
3576 au => ['card', 'billing_address', 'mailing_address']
3583 for my $evt (@{$self->events}) {
3586 $payload->{copy} = $U->unflesh_copy($self->copy);
3587 $payload->{volume} = $self->volume;
3588 $payload->{record} = $record,
3589 $payload->{circ} = $self->circ;
3590 $payload->{transit} = $self->transit;
3591 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3592 $payload->{hold} = $hold;
3593 $payload->{patron} = $self->patron;
3594 $payload->{reservation} = $self->reservation
3595 unless (not $self->reservation or $self->reservation->cancel_time);
3597 $evt->{payload} = $payload;
3602 my( $self, $msg ) = @_;
3603 my $bc = ($self->copy) ? $self->copy->barcode :
3606 my $usr = ($self->patron) ? $self->patron->id : "";
3607 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3608 ", recipient=$usr, copy=$bc");
3614 $self->log_me("do_renew()");
3616 # Make sure there is an open circ to renew that is not
3617 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3618 my $usrid = $self->patron->id if $self->patron;
3619 my $circ = $self->editor->search_action_circulation({
3620 target_copy => $self->copy->id,
3621 xact_finish => undef,
3622 checkin_time => undef,
3623 ($usrid ? (usr => $usrid) : ()),
3625 {stop_fines => undef},
3626 {stop_fines => OILS_STOP_FINES_MAX_FINES}
3630 return $self->bail_on_events($self->editor->event) unless $circ;
3632 # A user is not allowed to renew another user's items without permission
3633 unless( $circ->usr eq $self->editor->requestor->id ) {
3634 return $self->bail_on_events($self->editor->events)
3635 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3638 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3639 if $circ->renewal_remaining < 1;
3641 # -----------------------------------------------------------------
3643 $self->parent_circ($circ->id);
3644 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3647 # Opac renewal - re-use circ library from original circ (unless told not to)
3648 if($self->opac_renewal) {
3649 unless(defined($opac_renewal_use_circ_lib)) {
3650 my $use_circ_lib = $self->editor->retrieve_config_global_flag('circ.opac_renewal.use_original_circ_lib');
3651 if($use_circ_lib and $U->is_true($use_circ_lib->enabled)) {
3652 $opac_renewal_use_circ_lib = 1;
3655 $opac_renewal_use_circ_lib = 0;
3658 $self->circ_lib($circ->circ_lib) if($opac_renewal_use_circ_lib);
3661 # Run the fine generator against the old circ
3662 $self->generate_fines_start;
3664 $self->run_renew_permit;
3667 $self->do_checkin();
3668 return if $self->bail_out;
3670 unless( $self->permit_override ) {
3672 return if $self->bail_out;
3673 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3674 $self->remove_event('ITEM_NOT_CATALOGED');
3677 $self->override_events;
3678 return if $self->bail_out;
3681 $self->do_checkout();
3686 my( $self, $evt ) = @_;
3687 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3688 $logger->debug("circulator: removing event from list: $evt");
3689 my @events = @{$self->events};
3690 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3695 my( $self, $evt ) = @_;
3696 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3697 return grep { $_->{textcode} eq $evt } @{$self->events};
3702 sub run_renew_permit {
3705 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3706 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3707 $self->editor, $self->copy, $self->editor->requestor, 1
3709 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3712 if(!$self->legacy_script_support) {
3713 my $results = $self->run_indb_circ_test;
3714 $self->push_events($self->matrix_test_result_events)
3715 unless $self->circ_test_success;
3718 my $runner = $self->script_runner;
3720 $runner->load($self->circ_permit_renew);
3721 my $result = $runner->run or
3722 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3723 if ($result->{"events"}) {
3725 map { new OpenILS::Event($_) } @{$result->{"events"}}
3728 "circulator: circ_permit_renew for user " .
3729 $self->patron->id . " returned " .
3730 scalar(@{$result->{"events"}}) . " event(s)"
3734 $self->mk_script_runner;
3737 $logger->debug("circulator: re-creating script runner to be safe");
3741 # XXX: The primary mechanism for storing circ history is now handled
3742 # by tracking real circulation objects instead of bibs in a bucket.
3743 # However, this code is disabled by default and could be useful
3744 # some day, so may as well leave it for now.
3745 sub append_reading_list {
3749 $self->is_checkout and
3755 # verify history is globally enabled and uses the bucket mechanism
3756 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3757 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3759 return undef unless $htype and $htype eq 'bucket';
3761 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3763 # verify the patron wants to retain the hisory
3764 my $setting = $e->search_actor_user_setting(
3765 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3767 unless($setting and $setting->value) {
3772 my $bkt = $e->search_container_copy_bucket(
3773 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3778 # find the next item position
3779 my $last_item = $e->search_container_copy_bucket_item(
3780 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3781 $pos = $last_item->pos + 1 if $last_item;
3784 # create the history bucket if necessary
3785 $bkt = Fieldmapper::container::copy_bucket->new;
3786 $bkt->owner($self->patron->id);
3788 $bkt->btype('circ_history');
3790 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3793 my $item = Fieldmapper::container::copy_bucket_item->new;
3795 $item->bucket($bkt->id);
3796 $item->target_copy($self->copy->id);
3799 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3806 sub make_trigger_events {
3808 return unless $self->circ;
3809 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3810 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3811 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3816 sub checkin_handle_lost_now_found {
3817 my ($self, $bill_type) = @_;
3819 # ------------------------------------------------------------------
3820 # remove charge from patron's account if lost item is returned
3821 # ------------------------------------------------------------------
3823 my $bills = $self->editor->search_money_billing(
3825 xact => $self->circ->id,
3830 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3831 for my $bill (@$bills) {
3832 if( !$U->is_true($bill->voided) ) {
3833 $logger->info("lost item returned - voiding bill ".$bill->id);
3835 $bill->void_time('now');
3836 $bill->voider($self->editor->requestor->id);
3837 my $note = ($bill->note) ? $bill->note . "\n" : '';
3838 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3840 $self->bail_on_events($self->editor->event)
3841 unless $self->editor->update_money_billing($bill);
3846 sub checkin_handle_lost_now_found_restore_od {
3848 my $circ_lib = shift;
3850 # ------------------------------------------------------------------
3851 # restore those overdue charges voided when item was set to lost
3852 # ------------------------------------------------------------------
3854 my $ods = $self->editor->search_money_billing(
3856 xact => $self->circ->id,
3861 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3862 for my $bill (@$ods) {
3863 if( $U->is_true($bill->voided) ) {
3864 $logger->info("lost item returned - restoring overdue ".$bill->id);
3866 $bill->clear_void_time;
3867 $bill->voider($self->editor->requestor->id);
3868 my $note = ($bill->note) ? $bill->note . "\n" : '';
3869 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3871 $self->bail_on_events($self->editor->event)
3872 unless $self->editor->update_money_billing($bill);
3877 # ------------------------------------------------------------------
3878 # Lost-then-found item checked in. This sub generates new overdue
3879 # fines, beyond the point of any existing and possibly voided
3880 # overdue fines, up to the point of final checkin time (or max fine
3882 # ------------------------------------------------------------------
3883 sub generate_lost_overdue_fines {
3885 my $circ = $self->circ;
3886 my $e = $self->editor;
3888 # Re-open the transaction so the fine generator can see it
3889 if($circ->xact_finish or $circ->stop_fines) {
3891 $circ->clear_xact_finish;
3892 $circ->clear_stop_fines;
3893 $circ->clear_stop_fines_time;
3894 $e->update_action_circulation($circ) or return $e->die_event;
3898 $e->xact_begin; # generate_fines expects an in-xact editor
3899 $self->generate_fines;
3900 $circ = $self->circ; # generate fines re-fetches the circ
3904 # Re-close the transaction if no money is owed
3905 my ($obt) = $U->fetch_mbts($circ->id, $e);
3906 if ($obt and $obt->balance_owed == 0) {
3907 $circ->xact_finish('now');
3911 # Set stop fines if the fine generator didn't have to
3912 unless($circ->stop_fines) {
3913 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
3914 $circ->stop_fines_time('now');
3918 # update the event data sent to the caller within the transaction
3919 $self->checkin_flesh_events;
3922 $e->update_action_circulation($circ) or return $e->die_event;