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;
19 flesh_fields => {acp => ['call_number'], acn => ['record']}
25 my $conf = OpenSRF::Utils::SettingsClient->new;
26 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
28 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
29 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
31 my $lb = $conf->config_value( @pfx2, 'script_path' );
32 $lb = [ $lb ] unless ref($lb);
35 return unless $legacy_script_support;
37 my @pfx = ( @pfx2, "scripts" );
38 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
39 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
40 my $d = $conf->config_value( @pfx, 'circ_duration' );
41 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
42 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
43 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
45 $logger->error( "Missing circ script(s)" )
46 unless( $p and $c and $d and $f and $m and $pr );
48 $scripts{circ_permit_patron} = $p;
49 $scripts{circ_permit_copy} = $c;
50 $scripts{circ_duration} = $d;
51 $scripts{circ_recurring_fines} = $f;
52 $scripts{circ_max_fines} = $m;
53 $scripts{circ_permit_renew} = $pr;
56 "circulator: Loaded rules scripts for circ: " .
57 "circ permit patron = $p, ".
58 "circ permit copy = $c, ".
59 "circ duration = $d, ".
60 "circ recurring fines = $f, " .
61 "circ max fines = $m, ".
62 "circ renew permit = $pr. ".
64 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
69 __PACKAGE__->register_method(
70 method => "run_method",
71 api_name => "open-ils.circ.checkout.permit",
73 Determines if the given checkout can occur
74 @param authtoken The login session key
75 @param params A trailing hash of named params including
76 barcode : The copy barcode,
77 patron : The patron the checkout is occurring for,
78 renew : true or false - whether or not this is a renewal
79 @return The event that occurred during the permit check.
83 __PACKAGE__->register_method (
84 method => 'run_method',
85 api_name => 'open-ils.circ.checkout.permit.override',
86 signature => q/@see open-ils.circ.checkout.permit/,
90 __PACKAGE__->register_method(
91 method => "run_method",
92 api_name => "open-ils.circ.checkout",
95 @param authtoken The login session key
96 @param params A named hash of params including:
98 barcode If no copy is provided, the copy is retrieved via barcode
99 copyid If no copy or barcode is provide, the copy id will be use
100 patron The patron's id
101 noncat True if this is a circulation for a non-cataloted item
102 noncat_type The non-cataloged type id
103 noncat_circ_lib The location for the noncat circ.
104 precat The item has yet to be cataloged
105 dummy_title The temporary title of the pre-cataloded item
106 dummy_author The temporary authr of the pre-cataloded item
107 Default is the home org of the staff member
108 @return The SUCCESS event on success, any other event depending on the error
111 __PACKAGE__->register_method(
112 method => "run_method",
113 api_name => "open-ils.circ.checkin",
116 Generic super-method for handling all copies
117 @param authtoken The login session key
118 @param params Hash of named parameters including:
119 barcode - The copy barcode
120 force - If true, copies in bad statuses will be checked in and give good statuses
121 noop - don't capture holds or put items into transit
122 void_overdues - void all overdues for the circulation (aka amnesty)
127 __PACKAGE__->register_method(
128 method => "run_method",
129 api_name => "open-ils.circ.checkin.override",
130 signature => q/@see open-ils.circ.checkin/
133 __PACKAGE__->register_method(
134 method => "run_method",
135 api_name => "open-ils.circ.renew.override",
136 signature => q/@see open-ils.circ.renew/,
140 __PACKAGE__->register_method(
141 method => "run_method",
142 api_name => "open-ils.circ.renew",
143 notes => <<" NOTES");
144 PARAMS( authtoken, circ => circ_id );
145 open-ils.circ.renew(login_session, circ_object);
146 Renews the provided circulation. login_session is the requestor of the
147 renewal and if the logged in user is not the same as circ->usr, then
148 the logged in user must have RENEW_CIRC permissions.
151 __PACKAGE__->register_method(
152 method => "run_method",
153 api_name => "open-ils.circ.checkout.full"
155 __PACKAGE__->register_method(
156 method => "run_method",
157 api_name => "open-ils.circ.checkout.full.override"
159 __PACKAGE__->register_method(
160 method => "run_method",
161 api_name => "open-ils.circ.reservation.pickup"
163 __PACKAGE__->register_method(
164 method => "run_method",
165 api_name => "open-ils.circ.reservation.return"
167 __PACKAGE__->register_method(
168 method => "run_method",
169 api_name => "open-ils.circ.checkout.inspect",
170 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
175 my( $self, $conn, $auth, $args ) = @_;
176 translate_legacy_args($args);
177 my $api = $self->api_name;
180 OpenILS::Application::Circ::Circulator->new($auth, %$args);
182 return circ_events($circulator) if $circulator->bail_out;
184 # --------------------------------------------------------------------------
185 # First, check for a booking transit, as the barcode may not be a copy
186 # barcode, but a resource barcode, and nothing else in here will work
187 # --------------------------------------------------------------------------
189 if ((my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
190 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
191 if (@$resources) { # yes!
193 my $res_id_list = [ map { $_->id } @$resources ];
194 my $transit = $circulator->editor->search_action_reservation_transit_copy(
196 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
197 { order_by => { artc => 'source_send_time' }, limit => 1 }
199 )->[0]; # Any transit for this barcode?
201 if ($transit) { # yes! unwrap it.
203 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
204 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
206 my $success_event = new OpenILS::Event(
207 "SUCCESS", "payload" => {"reservation" => $reservation}
209 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
210 if (my $copy = $circulator->editor->search_asset_copy([
211 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
212 ])->[0]) { # got a copy
213 $copy->status( $transit->copy_status );
214 $copy->editor($circulator->editor->requestor->id);
215 $copy->edit_date('now');
216 $circulator->editor->update_asset_copy($copy);
217 $success_event->{"payload"}->{"record"} =
218 $U->record_to_mvr($copy->call_number->record);
219 $copy->call_number($copy->call_number->id);
220 $success_event->{"payload"}->{"copy"} = $copy;
224 $transit->dest_recv_time('now');
225 $circulator->editor->update_action_reservation_transit_copy( $transit );
227 $circulator->editor->commit;
228 # Formerly this branch just stopped here. Argh!
229 $conn->respond_complete($success_event);
237 # --------------------------------------------------------------------------
238 # Go ahead and load the script runner to make sure we have all
239 # of the objects we need
240 # --------------------------------------------------------------------------
242 # XXX I wanted to make this better so it might support blocking renewals
243 # if a reservation has been placed on an item, but that will need more
244 # design, as institutions will differ in their policy on that. In the
245 # meantime making sure we're trying some kind of checkin will at least
246 # keep OPAC renewals from breaking since patrons don't have VIEW_USER...
248 $circulator->is_res_checkin($circulator->is_checkin(1))
249 if $api =~ /reservation.return/ or (
250 $api =~ /checkin/ and $circulator->seems_like_reservation()
253 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
255 $circulator->is_renewal(1) if $api =~ /renew/;
256 $circulator->is_checkin(1) if $api =~ /checkin/;
258 $circulator->mk_env();
259 $circulator->noop if $circulator->claims_never_checked_out;
261 if($legacy_script_support and not $circulator->is_checkin) {
262 $circulator->mk_script_runner();
263 $circulator->legacy_script_support(1);
264 $circulator->circ_permit_patron($scripts{circ_permit_patron});
265 $circulator->circ_permit_copy($scripts{circ_permit_copy});
266 $circulator->circ_duration($scripts{circ_duration});
267 $circulator->circ_permit_renew($scripts{circ_permit_renew});
269 return circ_events($circulator) if $circulator->bail_out;
272 $circulator->override(1) if $api =~ /override/o;
274 if( $api =~ /checkout\.permit/ ) {
275 $circulator->do_permit();
277 } elsif( $api =~ /checkout.full/ ) {
279 # requesting a precat checkout implies that any required
280 # overrides have been performed. Go ahead and re-override.
281 $circulator->skip_permit_key(1);
282 $circulator->override(1) if $circulator->request_precat;
283 $circulator->do_permit();
284 $circulator->is_checkout(1);
285 unless( $circulator->bail_out ) {
286 $circulator->events([]);
287 $circulator->do_checkout();
290 } elsif( $circulator->is_res_checkout ) {
291 $circulator->do_reservation_pickup();
293 } elsif( $api =~ /inspect/ ) {
294 my $data = $circulator->do_inspect();
295 $circulator->editor->rollback;
298 } elsif( $api =~ /checkout/ ) {
299 $circulator->is_checkout(1);
300 $circulator->do_checkout();
302 } elsif( $circulator->is_res_checkin ) {
303 $circulator->do_reservation_return();
304 $circulator->do_checkin() if ($circulator->copy());
305 } elsif( $api =~ /checkin/ ) {
306 $circulator->do_checkin();
308 } elsif( $api =~ /renew/ ) {
309 $circulator->is_renewal(1);
310 $circulator->do_renew();
313 if( $circulator->bail_out ) {
316 # make sure no success event accidentally slip in
318 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
321 my @e = @{$circulator->events};
322 push( @ee, $_->{textcode} ) for @e;
323 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
325 $circulator->editor->rollback;
328 $circulator->editor->commit;
331 $circulator->script_runner->cleanup if $circulator->script_runner;
333 $conn->respond_complete(circ_events($circulator));
335 unless($circulator->bail_out) {
336 $circulator->do_hold_notify($circulator->notify_hold)
337 if $circulator->notify_hold;
338 $circulator->retarget_holds if $circulator->retarget;
339 $circulator->append_reading_list;
340 $circulator->make_trigger_events;
346 my @e = @{$circ->events};
347 # if we have multiple events, SUCCESS should not be one of them;
348 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
349 return (@e == 1) ? $e[0] : \@e;
353 sub translate_legacy_args {
356 if( $$args{barcode} ) {
357 $$args{copy_barcode} = $$args{barcode};
358 delete $$args{barcode};
361 if( $$args{copyid} ) {
362 $$args{copy_id} = $$args{copyid};
363 delete $$args{copyid};
366 if( $$args{patronid} ) {
367 $$args{patron_id} = $$args{patronid};
368 delete $$args{patronid};
371 if( $$args{patron} and !ref($$args{patron}) ) {
372 $$args{patron_id} = $$args{patron};
373 delete $$args{patron};
377 if( $$args{noncat} ) {
378 $$args{is_noncat} = $$args{noncat};
379 delete $$args{noncat};
382 if( $$args{precat} ) {
383 $$args{is_precat} = $$args{request_precat} = $$args{precat};
384 delete $$args{precat};
390 # --------------------------------------------------------------------------
391 # This package actually manages all of the circulation logic
392 # --------------------------------------------------------------------------
393 package OpenILS::Application::Circ::Circulator;
394 use strict; use warnings;
395 use vars q/$AUTOLOAD/;
397 use OpenILS::Utils::Fieldmapper;
398 use OpenSRF::Utils::Cache;
399 use Digest::MD5 qw(md5_hex);
400 use DateTime::Format::ISO8601;
401 use OpenILS::Utils::PermitHold;
402 use OpenSRF::Utils qw/:datetime/;
403 use OpenSRF::Utils::SettingsClient;
404 use OpenILS::Application::Circ::Holds;
405 use OpenILS::Application::Circ::Transit;
406 use OpenSRF::Utils::Logger qw(:logger);
407 use OpenILS::Utils::CStoreEditor qw/:funcs/;
408 use OpenILS::Application::Circ::ScriptBuilder;
409 use OpenILS::Const qw/:const/;
410 use OpenILS::Utils::Penalty;
411 use OpenILS::Application::Circ::CircCommon;
414 my $holdcode = "OpenILS::Application::Circ::Holds";
415 my $transcode = "OpenILS::Application::Circ::Transit";
421 # --------------------------------------------------------------------------
422 # Add a pile of automagic getter/setter methods
423 # --------------------------------------------------------------------------
424 my @AUTOLOAD_FIELDS = qw/
471 recurring_fines_level
483 cancelled_hold_transit
490 circ_matrix_matchpoint
492 legacy_script_support
502 claims_never_checked_out
511 my $type = ref($self) or die "$self is not an object";
513 my $name = $AUTOLOAD;
516 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
517 $logger->error("circulator: $type: invalid autoload field: $name");
518 die "$type: invalid autoload field: $name\n"
523 *{"${type}::${name}"} = sub {
526 $s->{$name} = $v if defined $v;
530 return $self->$name($data);
535 my( $class, $auth, %args ) = @_;
536 $class = ref($class) || $class;
537 my $self = bless( {}, $class );
540 $self->editor(new_editor(xact => 1, authtoken => $auth));
542 unless( $self->editor->checkauth ) {
543 $self->bail_on_events($self->editor->event);
547 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
549 $self->$_($args{$_}) for keys %args;
552 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
554 # if this is a renewal, default to desk_renewal
555 $self->desk_renewal(1) unless
556 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
558 $self->capture('') unless $self->capture;
560 unless(%user_groups) {
561 my $gps = $self->editor->retrieve_all_permission_grp_tree;
562 %user_groups = map { $_->id => $_ } @$gps;
569 # --------------------------------------------------------------------------
570 # True if we should discontinue processing
571 # --------------------------------------------------------------------------
573 my( $self, $bool ) = @_;
574 if( defined $bool ) {
575 $logger->info("circulator: BAILING OUT") if $bool;
576 $self->{bail_out} = $bool;
578 return $self->{bail_out};
583 my( $self, @evts ) = @_;
586 $e->{payload} = $self->copy if
587 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
589 $logger->info("circulator: pushing event ".$e->{textcode});
590 push( @{$self->events}, $e ) unless
591 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
597 return '' if $self->skip_permit_key;
598 my $key = md5_hex( time() . rand() . "$$" );
599 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
600 return $self->permit_key($key);
603 sub check_permit_key {
605 return 1 if $self->skip_permit_key;
606 my $key = $self->permit_key;
607 return 0 unless $key;
608 my $k = "oils_permit_key_$key";
609 my $one = $self->cache_handle->get_cache($k);
610 $self->cache_handle->delete_cache($k);
611 return ($one) ? 1 : 0;
614 sub seems_like_reservation {
617 # Some words about the following method:
618 # 1) It requires the VIEW_USER permission, but that's not an
619 # issue, right, since all staff should have that?
620 # 2) It returns only one reservation at a time, even if an item can be
621 # and is currently overbooked. Hmmm....
622 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
623 my $result = $booking_ses->request(
624 "open-ils.booking.reservations.by_returnable_resource_barcode",
625 $self->editor->authtoken,
628 $booking_ses->disconnect;
630 return $self->bail_on_events($result) if defined $U->event_code($result);
633 $self->reservation(shift @$result);
641 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
642 sub save_trimmed_copy {
643 my ($self, $copy) = @_;
646 $self->volume($copy->call_number);
647 $self->title($self->volume->record);
648 $self->copy->call_number($self->volume->id);
649 $self->volume->record($self->title->id);
650 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
651 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
652 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
653 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
659 my $e = $self->editor;
661 # --------------------------------------------------------------------------
662 # Grab the fleshed copy
663 # --------------------------------------------------------------------------
664 unless($self->is_noncat) {
667 $copy = $e->retrieve_asset_copy(
668 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
670 } elsif( $self->copy_barcode ) {
672 $copy = $e->search_asset_copy(
673 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
674 } elsif( $self->reservation ) {
675 my $res = $e->json_query(
677 "select" => {"acp" => ["id"]},
682 "field" => "barcode",
686 "field" => "current_resource"
694 "id" => (ref $self->reservation) ?
695 $self->reservation->id : $self->reservation
700 if (ref $res eq "ARRAY" and scalar @$res) {
701 $logger->info("circulator: mapped reservation " .
702 $self->reservation . " to copy " . $res->[0]->{"id"});
703 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
708 $self->save_trimmed_copy($copy);
710 # We can't renew if there is no copy
711 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
712 if $self->is_renewal;
717 # --------------------------------------------------------------------------
719 # --------------------------------------------------------------------------
723 flesh_fields => {au => [ qw/ card / ]}
726 if( $self->patron_id ) {
727 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
728 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
730 } elsif( $self->patron_barcode ) {
732 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
733 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
734 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
736 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
737 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
740 if( my $copy = $self->copy ) {
741 my $circs = $e->search_action_circulation(
742 {target_copy => $copy->id, checkin_time => undef});
744 if( my $circ = $circs->[0] ) {
745 $patron = $e->retrieve_actor_user([$circ->usr, $flesh])
751 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
752 unless $self->patron($patron) or $self->is_checkin;
754 unless($self->is_checkin) {
756 # Check for inactivity and patron reg. expiration
758 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
759 unless $U->is_true($patron->active);
761 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
762 unless $U->is_true($patron->card->active);
764 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
765 cleanse_ISO8601($patron->expire_date));
767 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
768 if( CORE::time > $expire->epoch ) ;
772 # --------------------------------------------------------------------------
773 # This builds the script runner environment and fetches most of the
775 # --------------------------------------------------------------------------
776 sub mk_script_runner {
782 qw/copy copy_barcode copy_id patron
783 patron_id patron_barcode volume title editor/;
785 # Translate our objects into the ScriptBuilder args hash
786 $$args{$_} = $self->$_() for @fields;
788 $args->{ignore_user_status} = 1 if $self->is_checkin;
789 $$args{fetch_patron_by_circ_copy} = 1;
790 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
792 if( my $pco = $self->pending_checkouts ) {
793 $logger->info("circulator: we were given a pending checkouts number of $pco");
794 $$args{patronItemsOut} = $pco;
797 # This fetches most of the objects we need
798 $self->script_runner(
799 OpenILS::Application::Circ::ScriptBuilder->build($args));
801 # Now we translate the ScriptBuilder objects back into self
802 $self->$_($$args{$_}) for @fields;
804 my @evts = @{$args->{_events}} if $args->{_events};
806 $logger->debug("circulator: script builder returned events: @evts") if @evts;
810 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
811 if(!$self->is_noncat and
813 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
817 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
818 return $self->bail_on_events(@e);
823 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
824 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
825 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
826 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
830 # We can't renew if there is no copy
831 return $self->bail_on_events(@evts) if
832 $self->is_renewal and !$self->copy;
834 # Set some circ-specific flags in the script environment
835 my $evt = "environment";
836 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
838 if( $self->is_noncat ) {
839 $self->script_runner->insert("$evt.isNonCat", 1);
840 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
843 if( $self->is_precat ) {
844 $self->script_runner->insert("environment.isPrecat", 1, 1);
847 $self->script_runner->add_path( $_ ) for @$script_libs;
852 # --------------------------------------------------------------------------
853 # Does the circ permit work
854 # --------------------------------------------------------------------------
858 $self->log_me("do_permit()");
860 unless( $self->editor->requestor->id == $self->patron->id ) {
861 return $self->bail_on_events($self->editor->event)
862 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
865 $self->check_captured_holds();
866 $self->do_copy_checks();
867 return if $self->bail_out;
868 $self->run_patron_permit_scripts();
869 $self->run_copy_permit_scripts()
870 unless $self->is_precat or $self->is_noncat;
871 $self->check_item_deposit_events();
872 $self->override_events();
873 return if $self->bail_out;
875 if($self->is_precat and not $self->request_precat) {
878 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
879 return $self->bail_out(1) unless $self->is_renewal;
883 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
886 sub check_item_deposit_events {
888 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
889 if $self->is_deposit and not $self->is_deposit_exempt;
890 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
891 if $self->is_rental and not $self->is_rental_exempt;
894 # returns true if the user is not required to pay deposits
895 sub is_deposit_exempt {
897 my $pid = (ref $self->patron->profile) ?
898 $self->patron->profile->id : $self->patron->profile;
899 my $groups = $U->ou_ancestor_setting_value(
900 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
901 for my $grp (@$groups) {
902 return 1 if $self->is_group_descendant($grp, $pid);
907 # returns true if the user is not required to pay rental fees
908 sub is_rental_exempt {
910 my $pid = (ref $self->patron->profile) ?
911 $self->patron->profile->id : $self->patron->profile;
912 my $groups = $U->ou_ancestor_setting_value(
913 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
914 for my $grp (@$groups) {
915 return 1 if $self->is_group_descendant($grp, $pid);
920 sub is_group_descendant {
921 my($self, $p_id, $c_id) = @_;
922 return 0 unless defined $p_id and defined $c_id;
923 return 1 if $c_id == $p_id;
924 while(my $grp = $user_groups{$c_id}) {
925 $c_id = $grp->parent;
926 return 0 unless defined $c_id;
927 return 1 if $c_id == $p_id;
932 sub check_captured_holds {
934 my $copy = $self->copy;
935 my $patron = $self->patron;
937 return undef unless $copy;
939 my $s = $U->copy_status($copy->status)->id;
940 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
941 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
943 # Item is on the holds shelf, make sure it's going to the right person
944 my $holds = $self->editor->search_action_hold_request(
947 current_copy => $copy->id ,
948 capture_time => { '!=' => undef },
949 cancel_time => undef,
950 fulfillment_time => undef
956 if( $holds and $$holds[0] ) {
957 return undef if $$holds[0]->usr == $patron->id;
960 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
962 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
968 my $copy = $self->copy;
971 my $stat = $U->copy_status($copy->status)->id;
973 # We cannot check out a copy if it is in-transit
974 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
975 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
978 $self->handle_claims_returned();
979 return if $self->bail_out;
981 # no claims returned circ was found, check if there is any open circ
982 unless( $self->is_renewal ) {
984 my $circs = $self->editor->search_action_circulation(
985 { target_copy => $copy->id, checkin_time => undef }
988 if(my $old_circ = $circs->[0]) { # an open circ was found
990 my $payload = {copy => $copy};
992 if($old_circ->usr == $self->patron->id) {
994 $payload->{old_circ} = $old_circ;
996 # If there is an open circulation on the checkout item and an auto-renew
997 # interval is defined, inform the caller that they should go
998 # ahead and renew the item instead of warning about open circulations.
1000 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1002 'circ.checkout_auto_renew_age',
1006 if($auto_renew_intvl) {
1007 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1008 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1010 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1011 $payload->{auto_renew} = 1;
1016 return $self->bail_on_events(
1017 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1023 my $LEGACY_CIRC_EVENT_MAP = {
1024 'no_item' => 'ITEM_NOT_CATALOGED',
1025 'actor.usr.barred' => 'PATRON_BARRED',
1026 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1027 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1028 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1029 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1030 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1031 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1032 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1033 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1037 # ---------------------------------------------------------------------
1038 # This pushes any patron-related events into the list but does not
1039 # set bail_out for any events
1040 # ---------------------------------------------------------------------
1041 sub run_patron_permit_scripts {
1043 my $runner = $self->script_runner;
1044 my $patronid = $self->patron->id;
1048 if(!$self->legacy_script_support) {
1050 my $results = $self->run_indb_circ_test;
1051 unless($self->circ_test_success) {
1052 # no_item result is OK during noncat checkout
1053 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1054 push @allevents, $self->matrix_test_result_events;
1060 # ---------------------------------------------------------------------
1061 # # Now run the patron permit script
1062 # ---------------------------------------------------------------------
1063 $runner->load($self->circ_permit_patron);
1064 my $result = $runner->run or
1065 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1067 my $patron_events = $result->{events};
1069 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1070 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1071 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1072 $penalties = $penalties->{fatal_penalties};
1074 for my $pen (@$penalties) {
1075 my $event = OpenILS::Event->new($pen->name);
1076 $event->{desc} = $pen->label;
1077 push(@allevents, $event);
1080 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1084 $_->{payload} = $self->copy if
1085 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1088 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1090 $self->push_events(@allevents);
1093 sub matrix_test_result_codes {
1095 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1098 sub matrix_test_result_events {
1101 my $event = new OpenILS::Event(
1102 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1104 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1106 } (@{$self->matrix_test_result});
1109 sub run_indb_circ_test {
1111 return $self->matrix_test_result if $self->matrix_test_result;
1113 my $dbfunc = ($self->is_renewal) ?
1114 'action.item_user_renew_test' : 'action.item_user_circ_test';
1116 if( $self->is_precat && $self->request_precat) {
1117 $self->make_precat_copy;
1118 return if $self->bail_out;
1121 my $results = $self->editor->json_query(
1125 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1131 $self->circ_test_success($U->is_true($results->[0]->{success}));
1133 if(my $mp = $results->[0]->{matchpoint}) {
1134 $logger->info("circulator: circ policy test found matchpoint $mp");
1135 $self->circ_matrix_matchpoint(
1136 $self->editor->retrieve_config_circ_matrix_matchpoint([
1139 flesh_fields => {ccmm =>
1140 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
1146 return $self->matrix_test_result($results);
1149 # ---------------------------------------------------------------------
1150 # given a use and copy, this will calculate the circulation policy
1151 # parameters. Only works with in-db circ.
1152 # ---------------------------------------------------------------------
1156 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1158 $self->run_indb_circ_test;
1161 circ_test_success => $self->circ_test_success,
1162 failure_events => [],
1163 failure_codes => [],
1164 matchpoint => $self->circ_matrix_matchpoint
1167 unless($self->circ_test_success) {
1168 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1169 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1172 if($self->circ_matrix_matchpoint) {
1173 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1174 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1175 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1177 my $policy = $self->get_circ_policy(
1178 $duration_rule, $recurring_fine_rule, $max_fine_rule);
1180 $$results{$_} = $$policy{$_} for keys %$policy;
1186 # ---------------------------------------------------------------------
1187 # Loads the circ policy info for duration, recurring fine, and max
1188 # fine based on the current copy
1189 # ---------------------------------------------------------------------
1190 sub get_circ_policy {
1191 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
1194 duration_rule => $duration_rule->name,
1195 recurring_fine_rule => $recurring_fine_rule->name,
1196 max_fine_rule => $max_fine_rule->name,
1197 max_fine => $self->get_max_fine_amount($max_fine_rule),
1198 fine_interval => $recurring_fine_rule->recurrence_interval,
1199 renewal_remaining => $duration_rule->max_renewals
1202 $policy->{duration} = $duration_rule->shrt
1203 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1204 $policy->{duration} = $duration_rule->normal
1205 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1206 $policy->{duration} = $duration_rule->extended
1207 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1209 $policy->{recurring_fine} = $recurring_fine_rule->low
1210 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1211 $policy->{recurring_fine} = $recurring_fine_rule->normal
1212 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1213 $policy->{recurring_fine} = $recurring_fine_rule->high
1214 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1219 sub get_max_fine_amount {
1221 my $max_fine_rule = shift;
1222 my $max_amount = $max_fine_rule->amount;
1224 # if is_percent is true then the max->amount is
1225 # use as a percentage of the copy price
1226 if ($U->is_true($max_fine_rule->is_percent)) {
1227 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1228 $max_amount = $price * $max_fine_rule->amount / 100;
1230 $U->ou_ancestor_setting_value(
1232 'circ.max_fine.cap_at_price',
1236 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1237 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1245 sub run_copy_permit_scripts {
1247 my $copy = $self->copy || return;
1248 my $runner = $self->script_runner;
1252 if(!$self->legacy_script_support) {
1253 my $results = $self->run_indb_circ_test;
1254 push @allevents, $self->matrix_test_result_events
1255 unless $self->circ_test_success;
1258 # ---------------------------------------------------------------------
1259 # Capture all of the copy permit events
1260 # ---------------------------------------------------------------------
1261 $runner->load($self->circ_permit_copy);
1262 my $result = $runner->run or
1263 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1264 my $copy_events = $result->{events};
1266 # ---------------------------------------------------------------------
1267 # Now collect all of the events together
1268 # ---------------------------------------------------------------------
1269 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1272 # See if this copy has an alert message
1273 my $ae = $self->check_copy_alert();
1274 push( @allevents, $ae ) if $ae;
1276 # uniquify the events
1277 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1278 @allevents = values %hash;
1280 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1282 $self->push_events(@allevents);
1286 sub check_copy_alert {
1288 return undef if $self->is_renewal;
1289 return OpenILS::Event->new(
1290 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1291 if $self->copy and $self->copy->alert_message;
1297 # --------------------------------------------------------------------------
1298 # If the call is overriding and has permissions to override every collected
1299 # event, the are cleared. Any event that the caller does not have
1300 # permission to override, will be left in the event list and bail_out will
1302 # XXX We need code in here to cancel any holds/transits on copies
1303 # that are being force-checked out
1304 # --------------------------------------------------------------------------
1305 sub override_events {
1307 my @events = @{$self->events};
1308 return unless @events;
1310 if(!$self->override) {
1311 return $self->bail_out(1)
1312 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1317 for my $e (@events) {
1318 my $tc = $e->{textcode};
1319 next if $tc eq 'SUCCESS';
1320 my $ov = "$tc.override";
1321 $logger->info("circulator: attempting to override event: $ov");
1323 return $self->bail_on_events($self->editor->event)
1324 unless( $self->editor->allowed($ov) );
1329 # --------------------------------------------------------------------------
1330 # If there is an open claimsreturn circ on the requested copy, close the
1331 # circ if overriding, otherwise bail out
1332 # --------------------------------------------------------------------------
1333 sub handle_claims_returned {
1335 my $copy = $self->copy;
1337 my $CR = $self->editor->search_action_circulation(
1339 target_copy => $copy->id,
1340 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1341 checkin_time => undef,
1345 return unless ($CR = $CR->[0]);
1349 # - If the caller has set the override flag, we will check the item in
1350 if($self->override) {
1352 $CR->checkin_time('now');
1353 $CR->checkin_scan_time('now');
1354 $CR->checkin_lib($self->circ_lib);
1355 $CR->checkin_workstation($self->editor->requestor->wsid);
1356 $CR->checkin_staff($self->editor->requestor->id);
1358 $evt = $self->editor->event
1359 unless $self->editor->update_action_circulation($CR);
1362 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1365 $self->bail_on_events($evt) if $evt;
1370 # --------------------------------------------------------------------------
1371 # This performs the checkout
1372 # --------------------------------------------------------------------------
1376 $self->log_me("do_checkout()");
1378 # make sure perms are good if this isn't a renewal
1379 unless( $self->is_renewal ) {
1380 return $self->bail_on_events($self->editor->event)
1381 unless( $self->editor->allowed('COPY_CHECKOUT') );
1384 # verify the permit key
1385 unless( $self->check_permit_key ) {
1386 if( $self->permit_override ) {
1387 return $self->bail_on_events($self->editor->event)
1388 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1390 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1394 # if this is a non-cataloged circ, build the circ and finish
1395 if( $self->is_noncat ) {
1396 $self->checkout_noncat;
1398 OpenILS::Event->new('SUCCESS',
1399 payload => { noncat_circ => $self->circ }));
1403 if( $self->is_precat ) {
1404 $self->make_precat_copy;
1405 return if $self->bail_out;
1407 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1408 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1411 $self->do_copy_checks;
1412 return if $self->bail_out;
1414 $self->run_checkout_scripts();
1415 return if $self->bail_out;
1417 $self->build_checkout_circ_object();
1418 return if $self->bail_out;
1420 my $modify_to_start = $self->booking_adjusted_due_date();
1421 return if $self->bail_out;
1423 $self->apply_modified_due_date($modify_to_start);
1424 return if $self->bail_out;
1426 return $self->bail_on_events($self->editor->event)
1427 unless $self->editor->create_action_circulation($self->circ);
1429 # refresh the circ to force local time zone for now
1430 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1432 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1434 return if $self->bail_out;
1436 $self->apply_deposit_fee();
1437 return if $self->bail_out;
1439 $self->handle_checkout_holds();
1440 return if $self->bail_out;
1442 # ------------------------------------------------------------------------------
1443 # Update the patron penalty info in the DB. Run it for permit-overrides
1444 # since the penalties are not updated during the permit phase
1445 # ------------------------------------------------------------------------------
1446 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1448 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1451 if($self->is_renewal) {
1452 # flesh the billing summary for the checked-in circ
1453 $pcirc = $self->editor->retrieve_action_circulation([
1455 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1460 OpenILS::Event->new('SUCCESS',
1462 copy => $U->unflesh_copy($self->copy),
1463 circ => $self->circ,
1465 holds_fulfilled => $self->fulfilled_holds,
1466 deposit_billing => $self->deposit_billing,
1467 rental_billing => $self->rental_billing,
1468 parent_circ => $pcirc,
1469 patron => ($self->return_patron) ? $self->patron : undef,
1470 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1476 sub apply_deposit_fee {
1478 my $copy = $self->copy;
1480 ($self->is_deposit and not $self->is_deposit_exempt) or
1481 ($self->is_rental and not $self->is_rental_exempt);
1483 return if $self->is_deposit and $self->skip_deposit_fee;
1484 return if $self->is_rental and $self->skip_rental_fee;
1486 my $bill = Fieldmapper::money::billing->new;
1487 my $amount = $copy->deposit_amount;
1491 if($self->is_deposit) {
1492 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1494 $self->deposit_billing($bill);
1496 $billing_type = OILS_BILLING_TYPE_RENTAL;
1498 $self->rental_billing($bill);
1501 $bill->xact($self->circ->id);
1502 $bill->amount($amount);
1503 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1504 $bill->billing_type($billing_type);
1505 $bill->btype($btype);
1506 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1508 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1513 my $copy = $self->copy;
1515 my $stat = $copy->status if ref $copy->status;
1516 my $loc = $copy->location if ref $copy->location;
1517 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1519 $copy->status($stat->id) if $stat;
1520 $copy->location($loc->id) if $loc;
1521 $copy->circ_lib($circ_lib->id) if $circ_lib;
1522 $copy->editor($self->editor->requestor->id);
1523 $copy->edit_date('now');
1524 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1526 return $self->bail_on_events($self->editor->event)
1527 unless $self->editor->update_asset_copy($self->copy);
1529 $copy->status($U->copy_status($copy->status));
1530 $copy->location($loc) if $loc;
1531 $copy->circ_lib($circ_lib) if $circ_lib;
1534 sub update_reservation {
1536 my $reservation = $self->reservation;
1538 my $usr = $reservation->usr;
1539 my $target_rt = $reservation->target_resource_type;
1540 my $target_r = $reservation->target_resource;
1541 my $current_r = $reservation->current_resource;
1543 $reservation->usr($usr->id) if ref $usr;
1544 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1545 $reservation->target_resource($target_r->id) if ref $target_r;
1546 $reservation->current_resource($current_r->id) if ref $current_r;
1548 return $self->bail_on_events($self->editor->event)
1549 unless $self->editor->update_booking_reservation($self->reservation);
1552 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1553 $self->reservation($reservation);
1557 sub bail_on_events {
1558 my( $self, @evts ) = @_;
1559 $self->push_events(@evts);
1564 # ------------------------------------------------------------------------------
1565 # When an item is checked out, see if we can fulfill a hold for this patron
1566 # ------------------------------------------------------------------------------
1567 sub handle_checkout_holds {
1569 my $copy = $self->copy;
1570 my $patron = $self->patron;
1572 my $e = $self->editor;
1573 $self->fulfilled_holds([]);
1575 # pre/non-cats can't fulfill a hold
1576 return if $self->is_precat or $self->is_noncat;
1578 my $hold = $e->search_action_hold_request({
1579 current_copy => $copy->id ,
1580 cancel_time => undef,
1581 fulfillment_time => undef,
1583 {expire_time => undef},
1584 {expire_time => {'>' => 'now'}}
1588 if($hold and $hold->usr != $patron->id) {
1589 # reset the hold since the copy is now checked out
1591 $logger->info("circulator: un-targeting hold ".$hold->id.
1592 " because copy ".$copy->id." is getting checked out");
1594 $hold->clear_prev_check_time;
1595 $hold->clear_current_copy;
1596 $hold->clear_capture_time;
1598 return $self->bail_on_event($e->event)
1599 unless $e->update_action_hold_request($hold);
1605 $hold = $self->find_related_user_hold($copy, $patron) or return;
1606 $logger->info("circulator: found related hold to fulfill in checkout");
1609 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1611 # if the hold was never officially captured, capture it.
1612 $hold->current_copy($copy->id);
1613 $hold->capture_time('now') unless $hold->capture_time;
1614 $hold->fulfillment_time('now');
1615 $hold->fulfillment_staff($e->requestor->id);
1616 $hold->fulfillment_lib($self->circ_lib);
1618 return $self->bail_on_events($e->event)
1619 unless $e->update_action_hold_request($hold);
1621 $holdcode->delete_hold_copy_maps($e, $hold->id);
1622 return $self->fulfilled_holds([$hold->id]);
1626 # ------------------------------------------------------------------------------
1627 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1628 # the patron directly targets the checked out item, see if there is another hold
1629 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1630 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1631 # ------------------------------------------------------------------------------
1632 sub find_related_user_hold {
1633 my($self, $copy, $patron) = @_;
1634 my $e = $self->editor;
1636 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1638 return undef unless $U->ou_ancestor_setting_value(
1639 $self->circ_lib, 'circ.checkout_fills_related_hold', $e);
1641 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1643 select => {ahr => ['id']},
1648 fkey => 'current_copy',
1649 type => 'left' # there may be no current_copy
1656 fulfillment_time => undef,
1657 cancel_time => undef,
1659 {expire_time => undef},
1660 {expire_time => {'>' => 'now'}}
1667 target => $self->volume->id
1673 target => $self->title->id
1679 {id => undef}, # left-join copy may be nonexistent
1680 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1684 order_by => {ahr => {request_time => {direction => 'asc'}}},
1688 my $hold_info = $e->json_query($args)->[0];
1689 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1694 sub run_checkout_scripts {
1699 my $runner = $self->script_runner;
1708 if(!$self->legacy_script_support) {
1709 $self->run_indb_circ_test();
1710 $duration = $self->circ_matrix_matchpoint->duration_rule;
1711 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1712 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1716 $runner->load($self->circ_duration);
1718 my $result = $runner->run or
1719 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1721 $duration_name = $result->{durationRule};
1722 $recurring_name = $result->{recurringFinesRule};
1723 $max_fine_name = $result->{maxFine};
1726 $duration_name = $duration->name if $duration;
1727 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1730 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1731 return $self->bail_on_events($evt) if ($evt && !$nobail);
1733 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1734 return $self->bail_on_events($evt) if ($evt && !$nobail);
1736 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1737 return $self->bail_on_events($evt) if ($evt && !$nobail);
1742 # The item circulates with an unlimited duration
1748 $self->duration_rule($duration);
1749 $self->recurring_fines_rule($recurring);
1750 $self->max_fine_rule($max_fine);
1754 sub build_checkout_circ_object {
1757 my $circ = Fieldmapper::action::circulation->new;
1758 my $duration = $self->duration_rule;
1759 my $max = $self->max_fine_rule;
1760 my $recurring = $self->recurring_fines_rule;
1761 my $copy = $self->copy;
1762 my $patron = $self->patron;
1766 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1768 my $dname = $duration->name;
1769 my $mname = $max->name;
1770 my $rname = $recurring->name;
1772 $logger->debug("circulator: building circulation ".
1773 "with duration=$dname, maxfine=$mname, recurring=$rname");
1775 $circ->duration($policy->{duration});
1776 $circ->recurring_fine($policy->{recurring_fine});
1777 $circ->duration_rule($duration->name);
1778 $circ->recurring_fine_rule($recurring->name);
1779 $circ->max_fine_rule($max->name);
1780 $circ->max_fine($policy->{max_fine});
1781 $circ->fine_interval($recurring->recurrence_interval);
1782 $circ->renewal_remaining($duration->max_renewals);
1786 $logger->info("circulator: copy found with an unlimited circ duration");
1787 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1788 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1789 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1790 $circ->renewal_remaining(0);
1793 $circ->target_copy( $copy->id );
1794 $circ->usr( $patron->id );
1795 $circ->circ_lib( $self->circ_lib );
1796 $circ->workstation($self->editor->requestor->wsid)
1797 if defined $self->editor->requestor->wsid;
1799 # renewals maintain a link to the parent circulation
1800 $circ->parent_circ($self->parent_circ);
1802 if( $self->is_renewal ) {
1803 $circ->opac_renewal('t') if $self->opac_renewal;
1804 $circ->phone_renewal('t') if $self->phone_renewal;
1805 $circ->desk_renewal('t') if $self->desk_renewal;
1806 $circ->renewal_remaining($self->renewal_remaining);
1807 $circ->circ_staff($self->editor->requestor->id);
1811 # if the user provided an overiding checkout time,
1812 # (e.g. the checkout really happened several hours ago), then
1813 # we apply that here. Does this need a perm??
1814 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1815 if $self->checkout_time;
1817 # if a patron is renewing, 'requestor' will be the patron
1818 $circ->circ_staff($self->editor->requestor->id);
1819 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1824 sub do_reservation_pickup {
1827 $self->log_me("do_reservation_pickup()");
1829 $self->reservation->pickup_time('now');
1832 $self->reservation->current_resource &&
1833 $self->reservation->current_resource->catalog_item
1835 $self->copy( $self->reservation->current_resource->catalog_item );
1836 $self->patron( $self->reservation->usr );
1837 $self->run_checkout_scripts(1);
1839 my $duration = $self->duration_rule;
1840 my $max = $self->max_fine_rule;
1841 my $recurring = $self->recurring_fines_rule;
1843 if ($duration && $max && $recurring) {
1844 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1846 my $dname = $duration->name;
1847 my $mname = $max->name;
1848 my $rname = $recurring->name;
1850 $logger->debug("circulator: building reservation ".
1851 "with duration=$dname, maxfine=$mname, recurring=$rname");
1853 $self->reservation->fine_amount($policy->{recurring_fine});
1854 $self->reservation->max_fine($policy->{max_fine});
1855 $self->reservation->fine_interval($recurring->recurrence_interval);
1858 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1859 $self->update_copy();
1862 $self->reservation->fine_amount($self->reservation->fine_amount);
1863 $self->reservation->max_fine($self->reservation->max_fine);
1864 $self->reservation->fine_interval($self->reservation->fine_interval);
1867 $self->update_reservation();
1870 sub do_reservation_return {
1872 my $request = shift;
1874 $self->log_me("do_reservation_return()");
1876 if (not ref $self->reservation) {
1877 my ($reservation, $evt) =
1878 $U->fetch_booking_reservation($self->reservation);
1879 return $self->bail_on_events($evt) if $evt;
1880 $self->reservation($reservation);
1883 $self->generate_fines(1);
1884 $self->reservation->return_time('now');
1885 $self->update_reservation();
1886 $self->reshelve_copy if $self->copy;
1888 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1889 $self->copy( $self->reservation->current_resource->catalog_item );
1893 sub booking_adjusted_due_date {
1895 my $circ = $self->circ;
1896 my $copy = $self->copy;
1901 if( $self->due_date ) {
1903 return $self->bail_on_events($self->editor->event)
1904 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1906 $circ->due_date(cleanse_ISO8601($self->due_date));
1910 return unless $copy and $circ->due_date;
1913 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1914 if (@$booking_items) {
1915 my $booking_item = $booking_items->[0];
1916 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1918 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1919 my $shorten_circ_setting = $resource_type->elbow_room ||
1920 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1923 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1924 my $bookings = $booking_ses->request(
1925 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1926 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1928 $booking_ses->disconnect;
1930 my $dt_parser = DateTime::Format::ISO8601->new;
1931 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1933 for my $bid (@$bookings) {
1935 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1937 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1938 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1940 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1941 if ($booking_start < DateTime->now);
1944 if ($U->is_true($stop_circ_setting)) {
1945 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
1947 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
1948 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
1951 # We set the circ duration here only to affect the logic that will
1952 # later (in a DB trigger) mangle the time part of the due date to
1953 # 11:59pm. Having any circ duration that is not a whole number of
1954 # days is enough to prevent the "correction."
1955 my $new_circ_duration = $due_date->epoch - time;
1956 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
1957 $circ->duration("$new_circ_duration seconds");
1959 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
1963 return $self->bail_on_events($self->editor->event)
1964 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1970 sub apply_modified_due_date {
1972 my $shift_earlier = shift;
1973 my $circ = $self->circ;
1974 my $copy = $self->copy;
1976 if( $self->due_date ) {
1978 return $self->bail_on_events($self->editor->event)
1979 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1981 $circ->due_date(cleanse_ISO8601($self->due_date));
1985 # if the due_date lands on a day when the location is closed
1986 return unless $copy and $circ->due_date;
1988 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1990 # due-date overlap should be determined by the location the item
1991 # is checked out from, not the owning or circ lib of the item
1992 my $org = $self->circ_lib;
1994 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1995 " with an item due date of ".$circ->due_date );
1997 my $dateinfo = $U->storagereq(
1998 'open-ils.storage.actor.org_unit.closed_date.overlap',
1999 $org, $circ->due_date );
2002 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2003 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2005 # XXX make the behavior more dynamic
2006 # for now, we just push the due date to after the close date
2007 if ($shift_earlier) {
2008 $circ->due_date($dateinfo->{start});
2010 $circ->due_date($dateinfo->{end});
2018 sub create_due_date {
2019 my( $self, $duration ) = @_;
2021 # if there is a raw time component (e.g. from postgres),
2022 # turn it into an interval that interval_to_seconds can parse
2023 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2025 # for now, use the server timezone. TODO: use workstation org timezone
2026 my $due_date = DateTime->now(time_zone => 'local');
2028 # add the circ duration
2029 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2031 # return ISO8601 time with timezone
2032 return $due_date->strftime('%FT%T%z');
2037 sub make_precat_copy {
2039 my $copy = $self->copy;
2042 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2044 $copy->editor($self->editor->requestor->id);
2045 $copy->edit_date('now');
2046 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2047 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2048 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2049 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2050 $self->update_copy();
2054 $logger->info("circulator: Creating a new precataloged ".
2055 "copy in checkout with barcode " . $self->copy_barcode);
2057 $copy = Fieldmapper::asset::copy->new;
2058 $copy->circ_lib($self->circ_lib);
2059 $copy->creator($self->editor->requestor->id);
2060 $copy->editor($self->editor->requestor->id);
2061 $copy->barcode($self->copy_barcode);
2062 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2063 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2064 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2066 $copy->dummy_title($self->dummy_title || "");
2067 $copy->dummy_author($self->dummy_author || "");
2068 $copy->dummy_isbn($self->dummy_isbn || "");
2069 $copy->circ_modifier($self->circ_modifier);
2072 # See if we need to override the circ_lib for the copy with a configured circ_lib
2073 # Setting is shortname of the org unit
2074 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2075 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2077 if($precat_circ_lib) {
2078 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2081 $self->bail_on_events($self->editor->event);
2085 $copy->circ_lib($org->id);
2089 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2091 $self->push_events($self->editor->event);
2095 # this is a little bit of a hack, but we need to
2096 # get the copy into the script runner
2097 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2101 sub checkout_noncat {
2107 my $lib = $self->noncat_circ_lib || $self->circ_lib;
2108 my $count = $self->noncat_count || 1;
2109 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2111 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2115 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2116 $self->editor->requestor->id,
2124 $self->push_events($evt);
2135 $self->log_me("do_checkin()");
2137 return $self->bail_on_events(
2138 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2141 if( $self->checkin_check_holds_shelf() ) {
2142 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2143 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2144 $self->checkin_flesh_events;
2148 unless( $self->is_renewal ) {
2149 return $self->bail_on_events($self->editor->event)
2150 unless $self->editor->allowed('COPY_CHECKIN');
2153 $self->push_events($self->check_copy_alert());
2154 $self->push_events($self->check_checkin_copy_status());
2156 # the renew code will have already found our circulation object
2157 unless( $self->is_renewal and $self->circ ) {
2158 my $circs = $self->editor->search_action_circulation(
2159 { target_copy => $self->copy->id, checkin_time => undef });
2160 $self->circ($$circs[0]);
2162 # for now, just warn if there are multiple open circs on a copy
2163 $logger->warn("circulator: we have ".scalar(@$circs).
2164 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2167 # run the fine generator against this circ, if this circ is there
2168 $self->generate_fines if ($self->circ);
2170 # if the circ is marked as 'claims returned', add the event to the list
2171 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2172 if ($self->circ and $self->circ->stop_fines
2173 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2175 $self->check_circ_deposit();
2177 # handle the overridable events
2178 $self->override_events unless $self->is_renewal;
2179 return if $self->bail_out;
2183 $self->editor->search_action_transit_copy(
2184 { target_copy => $self->copy->id, dest_recv_time => undef }
2190 $self->checkin_handle_circ;
2191 return if $self->bail_out;
2192 $self->checkin_changed(1);
2194 } elsif( $self->transit ) {
2195 my $hold_transit = $self->process_received_transit;
2196 $self->checkin_changed(1);
2198 if( $self->bail_out ) {
2199 $self->checkin_flesh_events;
2203 if( my $e = $self->check_checkin_copy_status() ) {
2204 # If the original copy status is special, alert the caller
2205 my $ev = $self->events;
2206 $self->events([$e]);
2207 $self->override_events;
2208 return if $self->bail_out;
2212 if( $hold_transit or
2213 $U->copy_status($self->copy->status)->id
2214 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2217 if( $hold_transit ) {
2218 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2220 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2225 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2227 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2228 $self->reshelve_copy(1);
2229 $self->cancelled_hold_transit(1);
2230 $self->notify_hold(0); # don't notify for cancelled holds
2231 return if $self->bail_out;
2235 # hold transited to correct location
2236 $self->checkin_flesh_events;
2241 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2243 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2244 " that is in-transit, but there is no transit.. repairing");
2245 $self->reshelve_copy(1);
2246 return if $self->bail_out;
2249 if( $self->is_renewal ) {
2250 $self->push_events(OpenILS::Event->new('SUCCESS'));
2254 # ------------------------------------------------------------------------------
2255 # Circulations and transits are now closed where necessary. Now go on to see if
2256 # this copy can fulfill a hold or needs to be routed to a different location
2257 # ------------------------------------------------------------------------------
2259 my $needed_for_something = 0; # formerly "needed_for_hold"
2261 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2263 if (!$self->remote_hold) {
2264 my $potential_hold = $self->hold_capture_is_possible;
2265 my $potential_reservation = $self->reservation_capture_is_possible;
2267 if ($potential_hold and $potential_reservation) {
2268 $logger->info("circulator: item could fulfill either hold or reservation");
2269 $self->push_events(new OpenILS::Event(
2270 "HOLD_RESERVATION_CONFLICT",
2271 "hold" => $potential_hold,
2272 "reservation" => $potential_reservation
2274 return if $self->bail_out;
2275 } elsif ($potential_hold) {
2276 $needed_for_something =
2277 $self->attempt_checkin_hold_capture;
2278 } elsif ($potential_reservation) {
2279 $needed_for_something =
2280 $self->attempt_checkin_reservation_capture;
2283 return if $self->bail_out;
2285 unless($needed_for_something) {
2286 my $circ_lib = (ref $self->copy->circ_lib) ?
2287 $self->copy->circ_lib->id : $self->copy->circ_lib;
2289 if( $self->remote_hold ) {
2290 $circ_lib = $self->remote_hold->pickup_lib;
2291 $logger->warn("circulator: Copy ".$self->copy->barcode.
2292 " is on a remote hold's shelf, sending to $circ_lib");
2295 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->circ_lib);
2297 if( $circ_lib == $self->circ_lib) {
2298 # copy is where it needs to be, either for hold or reshelving
2300 $self->checkin_handle_precat();
2301 return if $self->bail_out;
2304 # copy needs to transit "home", or stick here if it's a floating copy
2306 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2307 $self->checkin_changed(1);
2308 $self->copy->circ_lib( $self->circ_lib );
2311 my $bc = $self->copy->barcode;
2312 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2313 $self->checkin_build_copy_transit($circ_lib);
2314 return if $self->bail_out;
2315 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2319 } else { # no-op checkin
2320 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2321 $self->checkin_changed(1);
2322 $self->copy->circ_lib( $self->circ_lib );
2327 if($self->claims_never_checked_out and
2328 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2330 # the item was not supposed to be checked out to the user and should now be marked as missing
2331 $self->copy->status(OILS_COPY_STATUS_MISSING);
2335 $self->reshelve_copy unless $needed_for_something;
2338 return if $self->bail_out;
2340 unless($self->checkin_changed) {
2342 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2343 my $stat = $U->copy_status($self->copy->status)->id;
2345 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2346 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2347 $self->bail_out(1); # no need to commit anything
2351 $self->push_events(OpenILS::Event->new('SUCCESS'))
2352 unless @{$self->events};
2355 OpenILS::Utils::Penalty->calculate_penalties(
2356 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2358 $self->checkin_flesh_events;
2362 # if a deposit was payed for this item, push the event
2363 sub check_circ_deposit {
2365 return unless $self->circ;
2366 my $deposit = $self->editor->search_money_billing(
2368 xact => $self->circ->id,
2370 }, {idlist => 1})->[0];
2372 $self->push_events(OpenILS::Event->new(
2373 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2378 my $force = $self->force || shift;
2379 my $copy = $self->copy;
2381 my $stat = $U->copy_status($copy->status)->id;
2384 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2385 $stat != OILS_COPY_STATUS_CATALOGING and
2386 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2387 $stat != OILS_COPY_STATUS_RESHELVING )) {
2389 $copy->status( OILS_COPY_STATUS_RESHELVING );
2391 $self->checkin_changed(1);
2396 # Returns true if the item is at the current location
2397 # because it was transited there for a hold and the
2398 # hold has not been fulfilled
2399 sub checkin_check_holds_shelf {
2401 return 0 unless $self->copy;
2404 $U->copy_status($self->copy->status)->id ==
2405 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2407 # find the hold that put us on the holds shelf
2408 my $holds = $self->editor->search_action_hold_request(
2410 current_copy => $self->copy->id,
2411 capture_time => { '!=' => undef },
2412 fulfillment_time => undef,
2413 cancel_time => undef,
2418 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2419 $self->reshelve_copy(1);
2423 my $hold = $$holds[0];
2425 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2426 $hold->id. "] for copy ".$self->copy->barcode);
2428 if( $hold->pickup_lib == $self->circ_lib ) {
2429 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2433 $logger->info("circulator: hold is not for here..");
2434 $self->remote_hold($hold);
2439 sub checkin_handle_precat {
2441 my $copy = $self->copy;
2443 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2444 $copy->status(OILS_COPY_STATUS_CATALOGING);
2445 $self->update_copy();
2446 $self->checkin_changed(1);
2447 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2452 sub checkin_build_copy_transit {
2455 my $copy = $self->copy;
2456 my $transit = Fieldmapper::action::transit_copy->new;
2458 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2459 $logger->info("circulator: transiting copy to $dest");
2461 $transit->source($self->circ_lib);
2462 $transit->dest($dest);
2463 $transit->target_copy($copy->id);
2464 $transit->source_send_time('now');
2465 $transit->copy_status( $U->copy_status($copy->status)->id );
2467 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2469 return $self->bail_on_events($self->editor->event)
2470 unless $self->editor->create_action_transit_copy($transit);
2472 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2474 $self->checkin_changed(1);
2478 sub hold_capture_is_possible {
2480 my $copy = $self->copy;
2482 # we've been explicitly told not to capture any holds
2483 return 0 if $self->capture eq 'nocapture';
2485 # See if this copy can fulfill any holds
2486 my $hold = $holdcode->find_nearest_permitted_hold(
2487 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2489 return undef if ref $hold eq "HASH" and
2490 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2494 sub reservation_capture_is_possible {
2496 my $copy = $self->copy;
2498 # we've been explicitly told not to capture any holds
2499 return 0 if $self->capture eq 'nocapture';
2501 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2502 my $resv = $booking_ses->request(
2503 "open-ils.booking.reservations.could_capture",
2504 $self->editor->authtoken, $copy->barcode
2506 $booking_ses->disconnect;
2507 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2508 $self->push_events($resv);
2514 # returns true if the item was used (or may potentially be used
2515 # in subsequent calls) to capture a hold.
2516 sub attempt_checkin_hold_capture {
2518 my $copy = $self->copy;
2520 # we've been explicitly told not to capture any holds
2521 return 0 if $self->capture eq 'nocapture';
2523 # See if this copy can fulfill any holds
2524 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2525 $self->editor, $copy, $self->editor->requestor );
2528 $logger->debug("circulator: no potential permitted".
2529 "holds found for copy ".$copy->barcode);
2533 if($self->capture ne 'capture') {
2534 # see if this item is in a hold-capture-delay location
2535 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2536 if($U->is_true($location->hold_verify)) {
2537 $self->bail_on_events(
2538 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2543 $self->retarget($retarget);
2545 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2547 $hold->current_copy($copy->id);
2548 $hold->capture_time('now');
2549 $self->put_hold_on_shelf($hold)
2550 if $hold->pickup_lib == $self->circ_lib;
2552 # prevent DB errors caused by fetching
2553 # holds from storage, and updating through cstore
2554 $hold->clear_fulfillment_time;
2555 $hold->clear_fulfillment_staff;
2556 $hold->clear_fulfillment_lib;
2557 $hold->clear_expire_time;
2558 $hold->clear_cancel_time;
2559 $hold->clear_prev_check_time unless $hold->prev_check_time;
2561 $self->bail_on_events($self->editor->event)
2562 unless $self->editor->update_action_hold_request($hold);
2564 $self->checkin_changed(1);
2566 return 0 if $self->bail_out;
2568 if( $hold->pickup_lib == $self->circ_lib ) {
2570 # This hold was captured in the correct location
2571 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2572 $self->push_events(OpenILS::Event->new('SUCCESS'));
2574 #$self->do_hold_notify($hold->id);
2575 $self->notify_hold($hold->id);
2579 # Hold needs to be picked up elsewhere. Build a hold
2580 # transit and route the item.
2581 $self->checkin_build_hold_transit();
2582 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2583 return 0 if $self->bail_out;
2584 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2587 # make sure we save the copy status
2592 sub attempt_checkin_reservation_capture {
2594 my $copy = $self->copy;
2596 # we've been explicitly told not to capture any holds
2597 return 0 if $self->capture eq 'nocapture';
2599 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2600 my $evt = $booking_ses->request(
2601 "open-ils.booking.resources.capture_for_reservation",
2602 $self->editor->authtoken,
2604 1 # don't update copy - we probably have it locked
2606 $booking_ses->disconnect;
2608 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2610 "open-ils.booking.resources.capture_for_reservation " .
2611 "didn't return an event!"
2615 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2616 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2618 # not-transferable is an error event we'll pass on the user
2619 $logger->warn("reservation capture attempted against non-transferable item");
2620 $self->push_events($evt);
2622 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2623 # Re-retrieve copy as reservation capture may have changed
2624 # its status and whatnot.
2626 "circulator: booking capture win on copy " . $self->copy->id
2628 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2630 "circulator: changing copy " . $self->copy->id .
2631 "'s status from " . $self->copy->status . " to " .
2634 $self->copy->status($new_copy_status);
2637 $self->reservation($evt->{"payload"}->{"reservation"});
2639 if (exists $evt->{"payload"}->{"transit"}) {
2643 "org" => $evt->{"payload"}->{"transit"}->dest
2647 $self->checkin_changed(1);
2651 # other results are treated as "nothing to capture"
2655 sub do_hold_notify {
2656 my( $self, $holdid ) = @_;
2658 my $e = new_editor(xact => 1);
2659 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2661 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2662 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2664 $logger->info("circulator: running delayed hold notify process");
2666 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2667 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2669 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2670 hold_id => $holdid, requestor => $self->editor->requestor);
2672 $logger->debug("circulator: built hold notifier");
2674 if(!$notifier->event) {
2676 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2678 my $stat = $notifier->send_email_notify;
2679 if( $stat == '1' ) {
2680 $logger->info("circulator: hold notify succeeded for hold $holdid");
2684 $logger->debug("circulator: * hold notify cancelled or failed for hold $holdid");
2687 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2691 sub retarget_holds {
2693 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2694 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2695 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2696 # no reason to wait for the return value
2700 sub checkin_build_hold_transit {
2703 my $copy = $self->copy;
2704 my $hold = $self->hold;
2705 my $trans = Fieldmapper::action::hold_transit_copy->new;
2707 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2709 $trans->hold($hold->id);
2710 $trans->source($self->circ_lib);
2711 $trans->dest($hold->pickup_lib);
2712 $trans->source_send_time("now");
2713 $trans->target_copy($copy->id);
2715 # when the copy gets to its destination, it will recover
2716 # this status - put it onto the holds shelf
2717 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2719 return $self->bail_on_events($self->editor->event)
2720 unless $self->editor->create_action_hold_transit_copy($trans);
2725 sub process_received_transit {
2727 my $copy = $self->copy;
2728 my $copyid = $self->copy->id;
2730 my $status_name = $U->copy_status($copy->status)->name;
2731 $logger->debug("circulator: attempting transit receive on ".
2732 "copy $copyid. Copy status is $status_name");
2734 my $transit = $self->transit;
2736 if( $transit->dest != $self->circ_lib ) {
2737 # - this item is in-transit to a different location
2739 my $tid = $transit->id;
2740 my $loc = $self->circ_lib;
2741 my $dest = $transit->dest;
2743 $logger->info("circulator: Fowarding transit on copy which is destined ".
2744 "for a different location. transit=$tid, copy=$copyid, current ".
2745 "location=$loc, destination location=$dest");
2747 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2749 # grab the associated hold object if available
2750 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2751 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2753 return $self->bail_on_events($evt);
2756 # The transit is received, set the receive time
2757 $transit->dest_recv_time('now');
2758 $self->bail_on_events($self->editor->event)
2759 unless $self->editor->update_action_transit_copy($transit);
2761 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2763 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2764 $copy->status( $transit->copy_status );
2765 $self->update_copy();
2766 return if $self->bail_out;
2770 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2772 # hold has arrived at destination, set shelf time
2773 $self->put_hold_on_shelf($hold);
2774 $self->bail_on_events($self->editor->event)
2775 unless $self->editor->update_action_hold_request($hold);
2776 return if $self->bail_out;
2778 $self->notify_hold($hold_transit->hold);
2783 OpenILS::Event->new(
2786 payload => { transit => $transit, holdtransit => $hold_transit } ));
2788 return $hold_transit;
2792 # ------------------------------------------------------------------
2793 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2794 # ------------------------------------------------------------------
2795 sub put_hold_on_shelf {
2796 my($self, $hold) = @_;
2798 $hold->shelf_time('now');
2800 my $shelf_expire = $U->ou_ancestor_setting_value(
2801 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2804 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2805 my $expire_time = DateTime->now->add(seconds => $seconds);
2806 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2814 sub generate_fines {
2816 my $reservation = shift;
2820 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2822 my $st = OpenSRF::AppSession->connect('open-ils.storage');
2825 'open-ils.storage.action.circulation.overdue.generate_fines',
2832 # refresh the circ in case the fine generator set the stop_fines field
2833 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2834 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2839 sub checkin_handle_circ {
2841 my $circ = $self->circ;
2842 my $copy = $self->copy;
2846 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2848 # backdate the circ if necessary
2849 if($self->backdate) {
2850 $self->checkin_handle_backdate;
2851 return if $self->bail_out;
2854 if($self->void_overdues) {
2855 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2856 $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2857 return $self->bail_on_events($evt) if $evt;
2860 if(!$circ->stop_fines) {
2861 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2862 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2863 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2864 $circ->stop_fines_time('now');
2865 $circ->stop_fines_time($self->backdate) if $self->backdate;
2868 # Set the checkin vars since we have the item
2869 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2871 # capture the true scan time for back-dated checkins
2872 $circ->checkin_scan_time('now');
2874 $circ->checkin_staff($self->editor->requestor->id);
2875 $circ->checkin_lib($self->circ_lib);
2876 $circ->checkin_workstation($self->editor->requestor->wsid);
2878 my $circ_lib = (ref $self->copy->circ_lib) ?
2879 $self->copy->circ_lib->id : $self->copy->circ_lib;
2880 my $stat = $U->copy_status($self->copy->status)->id;
2882 # immediately available keeps items lost or missing items from going home before being handled
2883 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2884 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2887 if ( (!$lost_immediately_available) && ($circ_lib != $self->circ_lib) ) {
2889 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2890 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2892 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2896 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2898 $self->checkin_handle_lost($circ_lib);
2902 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2907 # see if there are any fines owed on this circ. if not, close it
2908 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2909 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2911 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2913 return $self->bail_on_events($self->editor->event)
2914 unless $self->editor->update_action_circulation($circ);
2916 # make sure the circ isn't closed if we just voided some fines
2917 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2918 return $self->bail_on_events($evt) if $evt;
2924 # ------------------------------------------------------------------
2925 # See if we need to void billings for lost checkin
2926 # ------------------------------------------------------------------
2927 sub checkin_handle_lost {
2929 my $circ_lib = shift;
2930 my $circ = $self->circ;
2932 my $max_return = $U->ou_ancestor_setting_value(
2933 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2938 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2939 $tm[5] -= 1 if $tm[5] > 0;
2940 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2942 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2943 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2945 $max_return = 0 if $today < $last_chance;
2948 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2950 my $void_lost = $U->ou_ancestor_setting_value(
2951 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2952 my $void_lost_fee = $U->ou_ancestor_setting_value(
2953 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2954 my $restore_od = $U->ou_ancestor_setting_value(
2955 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2957 $self->checkin_handle_lost_now_found(3) if $void_lost;
2958 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2959 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2962 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2967 sub checkin_handle_backdate {
2970 my $bd = cleanse_ISO8601($self->backdate);
2972 # ------------------------------------------------------------------
2973 # clean up the backdate for date comparison
2974 # we want any bills created on or after the backdate
2975 # ------------------------------------------------------------------
2976 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
2977 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
2978 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
2980 $self->backdate($bd);
2982 my $bills = $self->editor->search_money_billing(
2984 billing_ts => { '>=' => $bd },
2985 xact => $self->circ->id,
2990 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2992 for my $bill (@$bills) {
2993 unless( $U->is_true($bill->voided) ) {
2994 $logger->info("backdate voiding bill ".$bill->id);
2996 $bill->void_time('now');
2997 $bill->voider($self->editor->requestor->id);
2998 my $n = $bill->note || "";
2999 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
3001 $self->bail_on_events($self->editor->event)
3002 unless $self->editor->update_money_billing($bill);
3010 sub find_patron_from_copy {
3012 my $circs = $self->editor->search_action_circulation(
3013 { target_copy => $self->copy->id, checkin_time => undef });
3014 my $circ = $circs->[0];
3015 return unless $circ;
3016 my $u = $self->editor->retrieve_actor_user($circ->usr)
3017 or return $self->bail_on_events($self->editor->event);
3021 sub check_checkin_copy_status {
3023 my $copy = $self->copy;
3025 my $status = $U->copy_status($copy->status)->id;
3028 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3029 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3030 $status == OILS_COPY_STATUS_IN_PROCESS ||
3031 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3032 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3033 $status == OILS_COPY_STATUS_CATALOGING ||
3034 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3035 $status == OILS_COPY_STATUS_RESHELVING );
3037 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3038 if( $status == OILS_COPY_STATUS_LOST );
3040 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3041 if( $status == OILS_COPY_STATUS_MISSING );
3043 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3048 # --------------------------------------------------------------------------
3049 # On checkin, we need to return as many relevant objects as we can
3050 # --------------------------------------------------------------------------
3051 sub checkin_flesh_events {
3054 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3055 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3056 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3059 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3062 if($self->hold and !$self->hold->cancel_time) {
3063 $hold = $self->hold;
3064 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3068 # if we checked in a circulation, flesh the billing summary data
3069 $self->circ->billable_transaction(
3070 $self->editor->retrieve_money_billable_transaction([
3072 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3078 # flesh some patron fields before returning
3080 $self->editor->retrieve_actor_user([
3085 au => ['card', 'billing_address', 'mailing_address']
3092 for my $evt (@{$self->events}) {
3095 $payload->{copy} = $U->unflesh_copy($self->copy);
3096 $payload->{record} = $record,
3097 $payload->{circ} = $self->circ;
3098 $payload->{transit} = $self->transit;
3099 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3100 $payload->{hold} = $hold;
3101 $payload->{patron} = $self->patron;
3102 $payload->{reservation} = $self->reservation
3103 unless (not $self->reservation or $self->reservation->cancel_time);
3105 $evt->{payload} = $payload;
3110 my( $self, $msg ) = @_;
3111 my $bc = ($self->copy) ? $self->copy->barcode :
3114 my $usr = ($self->patron) ? $self->patron->id : "";
3115 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3116 ", recipient=$usr, copy=$bc");
3122 $self->log_me("do_renew()");
3124 # Make sure there is an open circ to renew that is not
3125 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3126 my $usrid = $self->patron->id if $self->patron;
3129 # If we have a patron, match them to the circ
3130 $circ = $self->editor->search_action_circulation(
3131 {target_copy => $self->copy->id, usr => $usrid, stop_fines => undef})->[0];
3133 $circ = $self->editor->search_action_circulation(
3134 {target_copy => $self->copy->id, stop_fines => undef})->[0];
3139 $circ = $self->editor->search_action_circulation(
3140 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
3142 $circ = $self->editor->search_action_circulation(
3143 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
3147 return $self->bail_on_events($self->editor->event) unless $circ;
3149 # A user is not allowed to renew another user's items without permission
3150 unless( $circ->usr eq $self->editor->requestor->id ) {
3151 return $self->bail_on_events($self->editor->events)
3152 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3155 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3156 if $circ->renewal_remaining < 1;
3158 # -----------------------------------------------------------------
3160 $self->parent_circ($circ->id);
3161 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3164 $self->run_renew_permit;
3167 $self->do_checkin();
3168 return if $self->bail_out;
3170 unless( $self->permit_override ) {
3172 return if $self->bail_out;
3173 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3174 $self->remove_event('ITEM_NOT_CATALOGED');
3177 $self->override_events;
3178 return if $self->bail_out;
3181 $self->do_checkout();
3186 my( $self, $evt ) = @_;
3187 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3188 $logger->debug("circulator: removing event from list: $evt");
3189 my @events = @{$self->events};
3190 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3195 my( $self, $evt ) = @_;
3196 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3197 return grep { $_->{textcode} eq $evt } @{$self->events};
3202 sub run_renew_permit {
3205 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3206 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3207 $self->editor, $self->copy, $self->editor->requestor, 1
3209 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3212 if(!$self->legacy_script_support) {
3213 my $results = $self->run_indb_circ_test;
3214 $self->push_events($self->matrix_test_result_events)
3215 unless $self->circ_test_success;
3218 my $runner = $self->script_runner;
3220 $runner->load($self->circ_permit_renew);
3221 my $result = $runner->run or
3222 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3223 if ($result->{"events"}) {
3225 map { new OpenILS::Event($_) } @{$result->{"events"}}
3228 "circulator: circ_permit_renew for user " .
3229 $self->patron->id . " returned " .
3230 scalar(@{$result->{"events"}}) . " event(s)"
3234 $self->mk_script_runner;
3237 $logger->debug("circulator: re-creating script runner to be safe");
3241 # XXX: The primary mechanism for storing circ history is now handled
3242 # by tracking real circulation objects instead of bibs in a bucket.
3243 # However, this code is disabled by default and could be useful
3244 # some day, so may as well leave it for now.
3245 sub append_reading_list {
3249 $self->is_checkout and
3255 # verify history is globally enabled and uses the bucket mechanism
3256 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3257 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3259 return undef unless $htype and $htype eq 'bucket';
3261 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3263 # verify the patron wants to retain the hisory
3264 my $setting = $e->search_actor_user_setting(
3265 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3267 unless($setting and $setting->value) {
3272 my $bkt = $e->search_container_copy_bucket(
3273 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3278 # find the next item position
3279 my $last_item = $e->search_container_copy_bucket_item(
3280 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3281 $pos = $last_item->pos + 1 if $last_item;
3284 # create the history bucket if necessary
3285 $bkt = Fieldmapper::container::copy_bucket->new;
3286 $bkt->owner($self->patron->id);
3288 $bkt->btype('circ_history');
3290 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3293 my $item = Fieldmapper::container::copy_bucket_item->new;
3295 $item->bucket($bkt->id);
3296 $item->target_copy($self->copy->id);
3299 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3306 sub make_trigger_events {
3308 return unless $self->circ;
3309 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3310 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3311 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3316 sub checkin_handle_lost_now_found {
3317 my ($self, $bill_type) = @_;
3319 # ------------------------------------------------------------------
3320 # remove charge from patron's account if lost item is returned
3321 # ------------------------------------------------------------------
3323 my $bills = $self->editor->search_money_billing(
3325 xact => $self->circ->id,
3330 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3331 for my $bill (@$bills) {
3332 if( !$U->is_true($bill->voided) ) {
3333 $logger->info("lost item returned - voiding bill ".$bill->id);
3335 $bill->void_time('now');
3336 $bill->voider($self->editor->requestor->id);
3337 my $note = ($bill->note) ? $bill->note . "\n" : '';
3338 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3340 $self->bail_on_events($self->editor->event)
3341 unless $self->editor->update_money_billing($bill);
3346 sub checkin_handle_lost_now_found_restore_od {
3349 # ------------------------------------------------------------------
3350 # restore those overdue charges voided when item was set to lost
3351 # ------------------------------------------------------------------
3353 my $ods = $self->editor->search_money_billing(
3355 xact => $self->circ->id,
3360 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3361 for my $bill (@$ods) {
3362 if( $U->is_true($bill->voided) ) {
3363 $logger->info("lost item returned - restoring overdue ".$bill->id);
3365 $bill->clear_void_time;
3366 $bill->voider($self->editor->requestor->id);
3367 my $note = ($bill->note) ? $bill->note . "\n" : '';
3368 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3370 $self->bail_on_events($self->editor->event)
3371 unless $self->editor->update_money_billing($bill);