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::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
8 use OpenILS::Application::AppUtils;
9 my $U = "OpenILS::Application::AppUtils";
13 my $legacy_script_support = 0;
18 my $conf = OpenSRF::Utils::SettingsClient->new;
19 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
20 my @pfx = ( @pfx2, "scripts" );
22 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
23 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
24 my $d = $conf->config_value( @pfx, 'circ_duration' );
25 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
26 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
27 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
28 my $lb = $conf->config_value( @pfx2, 'script_path' );
30 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
31 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
33 $logger->error( "Missing circ script(s)" )
34 unless( $p and $c and $d and $f and $m and $pr );
36 $scripts{circ_permit_patron} = $p;
37 $scripts{circ_permit_copy} = $c;
38 $scripts{circ_duration} = $d;
39 $scripts{circ_recurring_fines}= $f;
40 $scripts{circ_max_fines} = $m;
41 $scripts{circ_permit_renew} = $pr;
43 $lb = [ $lb ] unless ref($lb);
47 "circulator: Loaded rules scripts for circ: " .
48 "circ permit patron = $p, ".
49 "circ permit copy = $c, ".
50 "circ duration = $d, ".
51 "circ recurring fines = $f, " .
52 "circ max fines = $m, ".
53 "circ renew permit = $pr. ".
55 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
60 __PACKAGE__->register_method(
61 method => "run_method",
62 api_name => "open-ils.circ.checkout.permit",
64 Determines if the given checkout can occur
65 @param authtoken The login session key
66 @param params A trailing hash of named params including
67 barcode : The copy barcode,
68 patron : The patron the checkout is occurring for,
69 renew : true or false - whether or not this is a renewal
70 @return The event that occurred during the permit check.
74 __PACKAGE__->register_method (
75 method => 'run_method',
76 api_name => 'open-ils.circ.checkout.permit.override',
77 signature => q/@see open-ils.circ.checkout.permit/,
81 __PACKAGE__->register_method(
82 method => "run_method",
83 api_name => "open-ils.circ.checkout",
86 @param authtoken The login session key
87 @param params A named hash of params including:
89 barcode If no copy is provided, the copy is retrieved via barcode
90 copyid If no copy or barcode is provide, the copy id will be use
91 patron The patron's id
92 noncat True if this is a circulation for a non-cataloted item
93 noncat_type The non-cataloged type id
94 noncat_circ_lib The location for the noncat circ.
95 precat The item has yet to be cataloged
96 dummy_title The temporary title of the pre-cataloded item
97 dummy_author The temporary authr of the pre-cataloded item
98 Default is the home org of the staff member
99 @return The SUCCESS event on success, any other event depending on the error
102 __PACKAGE__->register_method(
103 method => "run_method",
104 api_name => "open-ils.circ.checkin",
107 Generic super-method for handling all copies
108 @param authtoken The login session key
109 @param params Hash of named parameters including:
110 barcode - The copy barcode
111 force - If true, copies in bad statuses will be checked in and give good statuses
116 __PACKAGE__->register_method(
117 method => "run_method",
118 api_name => "open-ils.circ.checkin.override",
119 signature => q/@see open-ils.circ.checkin/
122 __PACKAGE__->register_method(
123 method => "run_method",
124 api_name => "open-ils.circ.renew.override",
125 signature => q/@see open-ils.circ.renew/,
129 __PACKAGE__->register_method(
130 method => "run_method",
131 api_name => "open-ils.circ.renew",
132 notes => <<" NOTES");
133 PARAMS( authtoken, circ => circ_id );
134 open-ils.circ.renew(login_session, circ_object);
135 Renews the provided circulation. login_session is the requestor of the
136 renewal and if the logged in user is not the same as circ->usr, then
137 the logged in user must have RENEW_CIRC permissions.
140 __PACKAGE__->register_method(
141 method => "run_method",
142 api_name => "open-ils.circ.checkout.full");
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkout.full.override");
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.inspect",
151 Returns the circ matrix test result and, on success, the rule set and matrix test object
158 my( $self, $conn, $auth, $args ) = @_;
159 translate_legacy_args($args);
160 my $api = $self->api_name;
163 OpenILS::Application::Circ::Circulator->new($auth, %$args);
165 return circ_events($circulator) if $circulator->bail_out;
167 # --------------------------------------------------------------------------
168 # Go ahead and load the script runner to make sure we have all
169 # of the objects we need
170 # --------------------------------------------------------------------------
171 $circulator->is_renewal(1) if $api =~ /renew/;
172 $circulator->is_checkin(1) if $api =~ /checkin/;
173 $circulator->check_penalty_on_renew(1) if
174 $circulator->is_renewal and $U->ou_ancestor_setting_value(
175 $circulator->editor->requestor->ws_ou, 'circ.renew.check_penalty', $circulator->editor);
177 if($legacy_script_support and not $circulator->is_checkin) {
178 $circulator->mk_script_runner();
179 $circulator->legacy_script_support(1);
180 $circulator->circ_permit_patron($scripts{circ_permit_patron});
181 $circulator->circ_permit_copy($scripts{circ_permit_copy});
182 $circulator->circ_duration($scripts{circ_duration});
183 $circulator->circ_permit_renew($scripts{circ_permit_renew});
185 $circulator->mk_env();
187 return circ_events($circulator) if $circulator->bail_out;
190 $circulator->override(1) if $api =~ /override/o;
192 if( $api =~ /checkout\.permit/ ) {
193 $circulator->do_permit();
195 } elsif( $api =~ /checkout.full/ ) {
197 $circulator->do_permit();
198 unless( $circulator->bail_out ) {
199 $circulator->events([]);
200 $circulator->do_checkout();
203 } elsif( $api =~ /inspect/ ) {
204 my $data = $circulator->do_inspect();
205 $circulator->editor->rollback;
208 } elsif( $api =~ /checkout/ ) {
209 $circulator->do_checkout();
211 } elsif( $api =~ /checkin/ ) {
212 $circulator->do_checkin();
214 } elsif( $api =~ /renew/ ) {
215 $circulator->is_renewal(1);
216 $circulator->do_renew();
219 if( $circulator->bail_out ) {
222 # make sure no success event accidentally slip in
224 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
227 my @e = @{$circulator->events};
228 push( @ee, $_->{textcode} ) for @e;
229 $logger->info("circulator: bailing out with events: @ee");
231 $circulator->editor->rollback;
234 $circulator->editor->commit;
237 $circulator->script_runner->cleanup if $circulator->script_runner;
239 $conn->respond_complete(circ_events($circulator));
241 unless($circulator->bail_out) {
242 $circulator->do_hold_notify($circulator->notify_hold)
243 if $circulator->notify_hold;
244 $circulator->retarget_holds if $circulator->retarget;
250 my @e = @{$circ->events};
251 # if we have multiple events, SUCCESS should not be one of them;
252 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
253 return (@e == 1) ? $e[0] : \@e;
257 sub translate_legacy_args {
260 if( $$args{barcode} ) {
261 $$args{copy_barcode} = $$args{barcode};
262 delete $$args{barcode};
265 if( $$args{copyid} ) {
266 $$args{copy_id} = $$args{copyid};
267 delete $$args{copyid};
270 if( $$args{patronid} ) {
271 $$args{patron_id} = $$args{patronid};
272 delete $$args{patronid};
275 if( $$args{patron} and !ref($$args{patron}) ) {
276 $$args{patron_id} = $$args{patron};
277 delete $$args{patron};
281 if( $$args{noncat} ) {
282 $$args{is_noncat} = $$args{noncat};
283 delete $$args{noncat};
286 if( $$args{precat} ) {
287 $$args{is_precat} = $$args{precat};
288 delete $$args{precat};
294 # --------------------------------------------------------------------------
295 # This package actually manages all of the circulation logic
296 # --------------------------------------------------------------------------
297 package OpenILS::Application::Circ::Circulator;
298 use strict; use warnings;
299 use vars q/$AUTOLOAD/;
301 use OpenILS::Utils::Fieldmapper;
302 use OpenSRF::Utils::Cache;
303 use Digest::MD5 qw(md5_hex);
304 use DateTime::Format::ISO8601;
305 use OpenILS::Utils::PermitHold;
306 use OpenSRF::Utils qw/:datetime/;
307 use OpenSRF::Utils::SettingsClient;
308 use OpenILS::Application::Circ::Holds;
309 use OpenILS::Application::Circ::Transit;
310 use OpenSRF::Utils::Logger qw(:logger);
311 use OpenILS::Utils::CStoreEditor qw/:funcs/;
312 use OpenILS::Application::Circ::ScriptBuilder;
313 use OpenILS::Const qw/:const/;
315 my $holdcode = "OpenILS::Application::Circ::Holds";
316 my $transcode = "OpenILS::Application::Circ::Transit";
321 # --------------------------------------------------------------------------
322 # Add a pile of automagic getter/setter methods
323 # --------------------------------------------------------------------------
324 my @AUTOLOAD_FIELDS = qw/
339 check_penalty_on_renew
366 recurring_fines_level
379 cancelled_hold_transit
388 legacy_script_support
394 my $type = ref($self) or die "$self is not an object";
396 my $name = $AUTOLOAD;
399 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
400 $logger->error("circulator: $type: invalid autoload field: $name");
401 die "$type: invalid autoload field: $name\n"
406 *{"${type}::${name}"} = sub {
409 $s->{$name} = $v if defined $v;
413 return $self->$name($data);
418 my( $class, $auth, %args ) = @_;
419 $class = ref($class) || $class;
420 my $self = bless( {}, $class );
424 new_editor(xact => 1, authtoken => $auth) );
426 unless( $self->editor->checkauth ) {
427 $self->bail_on_events($self->editor->event);
431 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
433 $self->$_($args{$_}) for keys %args;
436 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
438 # if this is a renewal, default to desk_renewal
439 $self->desk_renewal(1) unless
440 $self->opac_renewal or $self->phone_renewal;
446 # --------------------------------------------------------------------------
447 # True if we should discontinue processing
448 # --------------------------------------------------------------------------
450 my( $self, $bool ) = @_;
451 if( defined $bool ) {
452 $logger->info("circulator: BAILING OUT") if $bool;
453 $self->{bail_out} = $bool;
455 return $self->{bail_out};
460 my( $self, @evts ) = @_;
463 $logger->info("circulator: pushing event ".$e->{textcode});
464 push( @{$self->events}, $e ) unless
465 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
471 my $key = md5_hex( time() . rand() . "$$" );
472 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
473 return $self->permit_key($key);
476 sub check_permit_key {
478 my $key = $self->permit_key;
479 return 0 unless $key;
480 my $k = "oils_permit_key_$key";
481 my $one = $self->cache_handle->get_cache($k);
482 $self->cache_handle->delete_cache($k);
483 return ($one) ? 1 : 0;
488 my $e = $self->editor;
490 # --------------------------------------------------------------------------
491 # Grab the fleshed copy
492 # --------------------------------------------------------------------------
493 unless($self->is_noncat) {
497 flesh_fields => {acp => ['call_number'], acn => ['record']}
500 $copy = $e->retrieve_asset_copy(
501 [$self->copy_id, $flesh ]) or return $e->event;
503 } elsif( $self->copy_barcode ) {
505 $copy = $e->search_asset_copy(
506 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0]
512 $self->volume($copy->call_number);
513 $self->title($self->volume->record);
514 $self->copy->call_number($self->volume->id);
515 $self->volume->record($self->title->id);
516 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
518 # We can't renew if there is no copy
519 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
520 if $self->is_renewal;
526 # --------------------------------------------------------------------------
528 # --------------------------------------------------------------------------
531 if( $self->patron_id ) {
532 $patron = $e->retrieve_actor_user($self->patron_id) or $p_evt = $e->event;
534 } elsif( $self->patron_barcode ) {
536 my $card = $e->search_actor_card(
537 {barcode => $self->patron_barcode})->[0] or return $e->event;
539 $patron = $e->search_actor_user(
540 {card => $card->id})->[0] or return $e->event;
543 if( my $copy = $self->copy ) {
544 my $circs = $e->search_action_circulation(
545 {target_copy => $copy->id, checkin_time => undef});
547 if( my $circ = $circs->[0] ) {
548 $patron = $e->retrieve_actor_user($circ->usr)
554 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
555 unless $self->patron($patron);
558 # --------------------------------------------------------------------------
559 # This builds the script runner environment and fetches most of the
561 # --------------------------------------------------------------------------
562 sub mk_script_runner {
568 qw/copy copy_barcode copy_id patron
569 patron_id patron_barcode volume title editor/;
571 # Translate our objects into the ScriptBuilder args hash
572 $$args{$_} = $self->$_() for @fields;
574 $args->{ignore_user_status} = 1 if $self->is_checkin;
575 $$args{fetch_patron_by_circ_copy} = 1;
576 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
578 if( my $pco = $self->pending_checkouts ) {
579 $logger->info("circulator: we were given a pending checkouts number of $pco");
580 $$args{patronItemsOut} = $pco;
583 # This fetches most of the objects we need
584 $self->script_runner(
585 OpenILS::Application::Circ::ScriptBuilder->build($args));
587 # Now we translate the ScriptBuilder objects back into self
588 $self->$_($$args{$_}) for @fields;
590 my @evts = @{$args->{_events}} if $args->{_events};
592 $logger->debug("circulator: script builder returned events: @evts") if @evts;
596 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
597 if(!$self->is_noncat and
599 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
603 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
604 return $self->bail_on_events(@e);
608 $self->is_precat(1) if $self->copy
609 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
611 # We can't renew if there is no copy
612 return $self->bail_on_events(@evts) if
613 $self->is_renewal and !$self->copy;
615 # Set some circ-specific flags in the script environment
616 my $evt = "environment";
617 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
619 if( $self->is_noncat ) {
620 $self->script_runner->insert("$evt.isNonCat", 1);
621 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
624 if( $self->is_precat ) {
625 $self->script_runner->insert("environment.isPrecat", 1, 1);
628 $self->script_runner->add_path( $_ ) for @$script_libs;
633 # --------------------------------------------------------------------------
634 # Does the circ permit work
635 # --------------------------------------------------------------------------
639 $self->log_me("do_permit()");
641 unless( $self->editor->requestor->id == $self->patron->id ) {
642 return $self->bail_on_events($self->editor->event)
643 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
646 $self->check_captured_holds();
647 $self->do_copy_checks();
648 return if $self->bail_out;
649 $self->run_patron_permit_scripts();
650 $self->run_copy_permit_scripts()
651 unless $self->is_precat or $self->is_noncat;
652 $self->override_events() unless
653 $self->is_renewal and not $self->check_penalty_on_renew;
654 return if $self->bail_out;
656 if( $self->is_precat ) {
659 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
660 return $self->bail_out(1) unless $self->is_renewal;
666 payload => $self->mk_permit_key));
670 sub check_captured_holds {
672 my $copy = $self->copy;
673 my $patron = $self->patron;
675 return undef unless $copy;
677 my $s = $U->copy_status($copy->status)->id;
678 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
679 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
681 # Item is on the holds shelf, make sure it's going to the right person
682 my $holds = $self->editor->search_action_hold_request(
685 current_copy => $copy->id ,
686 capture_time => { '!=' => undef },
687 cancel_time => undef,
688 fulfillment_time => undef
694 if( $holds and $$holds[0] ) {
695 return undef if $$holds[0]->usr == $patron->id;
698 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
700 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
706 my $copy = $self->copy;
709 my $stat = $U->copy_status($copy->status)->id;
711 # We cannot check out a copy if it is in-transit
712 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
713 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
716 $self->handle_claims_returned();
717 return if $self->bail_out;
719 # no claims returned circ was found, check if there is any open circ
720 unless( $self->is_renewal ) {
721 my $circs = $self->editor->search_action_circulation(
722 { target_copy => $copy->id, checkin_time => undef }
725 return $self->bail_on_events(
726 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
731 sub send_penalty_request {
733 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
734 $self->penalty_request(
736 'open-ils.penalty.patron_penalty.calculate',
738 authtoken => $self->editor->authtoken,
739 patron => $self->patron } ) );
742 sub gather_penalty_request {
744 return [] unless $self->penalty_request;
745 my $data = $self->penalty_request->recv;
747 throw $data if UNIVERSAL::isa($data,'Error');
748 $data = $data->content;
749 return $data->{fatal_penalties};
751 $logger->error("circulator: penalty request returned no data");
755 my $LEGACY_CIRC_EVENT_MAP = {
756 'actor.usr.barred' => 'PATRON_BARRED',
757 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
758 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
759 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
760 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
761 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
762 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
763 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
764 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
768 # ---------------------------------------------------------------------
769 # This pushes any patron-related events into the list but does not
770 # set bail_out for any events
771 # ---------------------------------------------------------------------
772 sub run_patron_permit_scripts {
774 my $runner = $self->script_runner;
775 my $patronid = $self->patron->id;
779 if(!$self->legacy_script_support) {
781 my $results = $self->run_indb_circ_test;
782 unless($self->circ_test_success) {
783 push(@allevents, OpenILS::Event->new(
784 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
789 $self->send_penalty_request() unless
790 $self->is_renewal and not $self->check_penalty_on_renew;
793 # --------------------------------------------------------------------- # Now run the patron permit script
794 # ---------------------------------------------------------------------
795 $runner->load($self->circ_permit_patron);
796 my $result = $runner->run or
797 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
799 my $patron_events = $result->{events};
801 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
802 [] : $self->gather_penalty_request();
804 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
807 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
809 $self->push_events(@allevents);
812 sub run_indb_circ_test {
814 return $self->matrix_test_result if $self->matrix_test_result;
816 my $dbfunc = ($self->is_renewal) ?
817 'action.item_user_renew_test' : 'action.item_user_circ_test';
819 my $results = ($self->matrix_test_result) ?
820 $self->matrix_test_result :
821 $self->editor->json_query(
824 $self->editor->requestor->ws_ou,
831 $self->circ_test_success($U->is_true($results->[0]->{success}));
832 if($self->circ_test_success) {
833 $self->circ_matrix_test(
834 $self->editor->retrieve_config_circ_matrix_test(
835 $results->[0]->{matchpoint}
840 if($self->circ_test_success) {
841 $self->circ_matrix_ruleset(
842 $self->editor->retrieve_config_circ_matrix_ruleset([
843 $results->[0]->{matchpoint},
846 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
854 return $self->matrix_test_result($results);
859 $self->run_indb_circ_test;
862 circ_test_success => $self->circ_test_success,
863 failure_events => [],
867 unless($self->circ_test_success) {
868 push(@{$results->{failure_codes}},
869 $_->{fail_part}) for @{$self->matrix_test_result};
870 push(@{$results->{failure_events}},
871 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
872 for @{$self->matrix_test_result};
876 # XXX lot of duplicated code here. extract this out to a
879 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
880 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
881 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
883 $results->{duration_rule} = $duration_rule->name;
884 $results->{recurring_fine_rule} = $recurring_fine_rule->name;
885 $results->{max_fine_rule} =$max_fine_rule->name;
886 $results->{max_fine} = $max_fine_rule->amount; # XXX support for price percents
887 $results->{fine_interval} = $recurring_fine_rule->recurance_interval;
888 $results->{renewal_remaining} = $duration_rule->max_renewals;
890 $results->{duration} = $duration_rule->shrt
891 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
892 $results->{duration} = $duration_rule->normal
893 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
894 $results->{duration} = $duration_rule->extended
895 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
897 $results->{recurring_fine} = $recurring_fine_rule->low
898 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
899 $results->{recurring_fine} = $recurring_fine_rule->normal
900 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
901 $results->{recurring_fine} = $recurring_fine_rule->high
902 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
909 sub run_copy_permit_scripts {
911 my $copy = $self->copy || return;
912 my $runner = $self->script_runner;
916 if(!$self->legacy_script_support) {
917 my $results = $self->run_indb_circ_test;
918 unless($self->circ_test_success) {
919 push(@allevents, OpenILS::Event->new(
920 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
924 # ---------------------------------------------------------------------
925 # Capture all of the copy permit events
926 # ---------------------------------------------------------------------
927 $runner->load($self->circ_permit_copy);
928 my $result = $runner->run or
929 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
930 my $copy_events = $result->{events};
932 # ---------------------------------------------------------------------
933 # Now collect all of the events together
934 # ---------------------------------------------------------------------
935 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
938 # See if this copy has an alert message
939 my $ae = $self->check_copy_alert();
940 push( @allevents, $ae ) if $ae;
942 # uniquify the events
943 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
944 @allevents = values %hash;
947 $_->{payload} = $copy if
948 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
951 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
953 $self->push_events(@allevents);
957 sub check_copy_alert {
959 return undef if $self->is_renewal;
960 return OpenILS::Event->new(
961 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
962 if $self->copy and $self->copy->alert_message;
968 # --------------------------------------------------------------------------
969 # If the call is overriding and has permissions to override every collected
970 # event, the are cleared. Any event that the caller does not have
971 # permission to override, will be left in the event list and bail_out will
973 # XXX We need code in here to cancel any holds/transits on copies
974 # that are being force-checked out
975 # --------------------------------------------------------------------------
976 sub override_events {
978 my @events = @{$self->events};
979 return unless @events;
981 if(!$self->override) {
982 return $self->bail_out(1)
983 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
988 for my $e (@events) {
989 my $tc = $e->{textcode};
990 next if $tc eq 'SUCCESS';
991 my $ov = "$tc.override";
992 $logger->info("circulator: attempting to override event: $ov");
994 return $self->bail_on_events($self->editor->event)
995 unless( $self->editor->allowed($ov) );
1000 # --------------------------------------------------------------------------
1001 # If there is an open claimsreturn circ on the requested copy, close the
1002 # circ if overriding, otherwise bail out
1003 # --------------------------------------------------------------------------
1004 sub handle_claims_returned {
1006 my $copy = $self->copy;
1008 my $CR = $self->editor->search_action_circulation(
1010 target_copy => $copy->id,
1011 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1012 checkin_time => undef,
1016 return unless ($CR = $CR->[0]);
1020 # - If the caller has set the override flag, we will check the item in
1021 if($self->override) {
1023 $CR->checkin_time('now');
1024 $CR->checkin_lib($self->editor->requestor->ws_ou);
1025 $CR->checkin_staff($self->editor->requestor->id);
1027 $evt = $self->editor->event
1028 unless $self->editor->update_action_circulation($CR);
1031 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1034 $self->bail_on_events($evt) if $evt;
1039 # --------------------------------------------------------------------------
1040 # This performs the checkout
1041 # --------------------------------------------------------------------------
1045 $self->log_me("do_checkout()");
1047 # make sure perms are good if this isn't a renewal
1048 unless( $self->is_renewal ) {
1049 return $self->bail_on_events($self->editor->event)
1050 unless( $self->editor->allowed('COPY_CHECKOUT') );
1053 # verify the permit key
1054 unless( $self->check_permit_key ) {
1055 if( $self->permit_override ) {
1056 return $self->bail_on_events($self->editor->event)
1057 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1059 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1063 # if this is a non-cataloged circ, build the circ and finish
1064 if( $self->is_noncat ) {
1065 $self->checkout_noncat;
1067 OpenILS::Event->new('SUCCESS',
1068 payload => { noncat_circ => $self->circ }));
1072 if( $self->is_precat ) {
1073 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1074 $self->make_precat_copy;
1075 return if $self->bail_out;
1077 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1078 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1081 $self->do_copy_checks;
1082 return if $self->bail_out;
1084 $self->run_checkout_scripts();
1085 return if $self->bail_out;
1087 $self->build_checkout_circ_object();
1088 return if $self->bail_out;
1090 $self->apply_modified_due_date();
1091 return if $self->bail_out;
1093 return $self->bail_on_events($self->editor->event)
1094 unless $self->editor->create_action_circulation($self->circ);
1096 # refresh the circ to force local time zone for now
1097 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1099 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1101 return if $self->bail_out;
1103 $self->handle_checkout_holds();
1104 return if $self->bail_out;
1106 # ------------------------------------------------------------------------------
1107 # Update the patron penalty info in the DB. Run it for permit-overrides or
1108 # renewals since both of those cases do not require the penalty server to
1109 # run during the permit phase of the checkout
1110 # ------------------------------------------------------------------------------
1111 if( $self->permit_override or $self->is_renewal ) {
1112 $U->update_patron_penalties(
1113 authtoken => $self->editor->authtoken,
1114 patron => $self->patron,
1119 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1121 OpenILS::Event->new('SUCCESS',
1123 copy => $U->unflesh_copy($self->copy),
1124 circ => $self->circ,
1126 holds_fulfilled => $self->fulfilled_holds,
1134 my $copy = $self->copy;
1136 my $stat = $copy->status if ref $copy->status;
1137 my $loc = $copy->location if ref $copy->location;
1138 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1140 $copy->status($stat->id) if $stat;
1141 $copy->location($loc->id) if $loc;
1142 $copy->circ_lib($circ_lib->id) if $circ_lib;
1143 $copy->editor($self->editor->requestor->id);
1144 $copy->edit_date('now');
1145 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1147 return $self->bail_on_events($self->editor->event)
1148 unless $self->editor->update_asset_copy($self->copy);
1150 $copy->status($U->copy_status($copy->status));
1151 $copy->location($loc) if $loc;
1152 $copy->circ_lib($circ_lib) if $circ_lib;
1156 sub bail_on_events {
1157 my( $self, @evts ) = @_;
1158 $self->push_events(@evts);
1162 sub handle_checkout_holds {
1165 my $copy = $self->copy;
1166 my $patron = $self->patron;
1168 my $holds = $self->editor->search_action_hold_request(
1170 current_copy => $copy->id ,
1171 cancel_time => undef,
1172 fulfillment_time => undef
1178 # XXX We should only fulfill one hold here...
1179 # XXX If a hold was transited to the user who is checking out
1180 # the item, we need to make sure that hold is what's grabbed
1183 # for now, just sort by id to get what should be the oldest hold
1184 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1185 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1186 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1189 my $hold = $myholds[0];
1191 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1193 # if the hold was never officially captured, capture it.
1194 $hold->capture_time('now') unless $hold->capture_time;
1196 # just make sure it's set correctly
1197 $hold->current_copy($copy->id);
1199 $hold->fulfillment_time('now');
1200 $hold->fulfillment_staff($self->editor->requestor->id);
1201 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1203 return $self->bail_on_events($self->editor->event)
1204 unless $self->editor->update_action_hold_request($hold);
1206 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1208 push( @fulfilled, $hold->id );
1211 # If there are any holds placed for other users that point to this copy,
1212 # then we need to un-target those holds so the targeter can pick a new copy
1215 $logger->info("circulator: un-targeting hold ".$_->id.
1216 " because copy ".$copy->id." is getting checked out");
1218 # - make the targeter process this hold at next run
1219 $_->clear_prev_check_time;
1221 # - clear out the targetted copy
1222 $_->clear_current_copy;
1223 $_->clear_capture_time;
1225 return $self->bail_on_event($self->editor->event)
1226 unless $self->editor->update_action_hold_request($_);
1230 $self->fulfilled_holds(\@fulfilled);
1235 sub run_checkout_scripts {
1239 my $runner = $self->script_runner;
1248 if(!$self->legacy_script_support) {
1249 $self->run_indb_circ_test();
1250 $duration = $self->circ_matrix_ruleset->duration_rule;
1251 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1252 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1256 $runner->load($self->circ_duration);
1258 my $result = $runner->run or
1259 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1261 $duration_name = $result->{durationRule};
1262 $recurring_name = $result->{recurringFinesRule};
1263 $max_fine_name = $result->{maxFine};
1266 $duration_name = $duration->name if $duration;
1267 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1270 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1271 return $self->bail_on_events($evt) if $evt;
1273 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1274 return $self->bail_on_events($evt) if $evt;
1276 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1277 return $self->bail_on_events($evt) if $evt;
1282 # The item circulates with an unlimited duration
1288 $self->duration_rule($duration);
1289 $self->recurring_fines_rule($recurring);
1290 $self->max_fine_rule($max_fine);
1294 sub build_checkout_circ_object {
1297 my $circ = Fieldmapper::action::circulation->new;
1298 my $duration = $self->duration_rule;
1299 my $max = $self->max_fine_rule;
1300 my $recurring = $self->recurring_fines_rule;
1301 my $copy = $self->copy;
1302 my $patron = $self->patron;
1306 my $dname = $duration->name;
1307 my $mname = $max->name;
1308 my $rname = $recurring->name;
1310 my $max_amount = $max->amount;
1312 # if is_percent is true then the max->amount is
1313 # use as a percentage of the copy price
1314 if ($U->is_true($max->is_percent)) {
1316 my $cn = $self->editor->retrieve_asset_call_number($copy->call_number);
1318 my $default_price = $U->ou_ancestor_setting_value(
1319 $cn->owning_lib, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
1320 my $charge_on_0 = $U->ou_ancestor_setting_value(
1321 $cn->owning_lib, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
1323 # Find the most appropriate "price" -- same definition as the
1324 # LOST price. See OpenILS::Circ::new_set_circ_lost
1325 $max_amount = $copy->price;
1326 $max_amount = $default_price unless defined $max_amount;
1327 $max_amount = 0 if $max_amount < 0;
1328 $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
1330 $max_amount *= $max->amount / 100;
1334 $logger->debug("circulator: building circulation ".
1335 "with duration=$dname, maxfine=$mname, recurring=$rname");
1337 $circ->duration( $duration->shrt )
1338 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1339 $circ->duration( $duration->normal )
1340 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1341 $circ->duration( $duration->extended )
1342 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1344 $circ->recuring_fine( $recurring->low )
1345 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1346 $circ->recuring_fine( $recurring->normal )
1347 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1348 $circ->recuring_fine( $recurring->high )
1349 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1351 $circ->duration_rule( $duration->name );
1352 $circ->recuring_fine_rule( $recurring->name );
1353 $circ->max_fine_rule( $max->name );
1355 $circ->max_fine( $max_amount );
1357 $circ->fine_interval($recurring->recurance_interval);
1358 $circ->renewal_remaining( $duration->max_renewals );
1362 $logger->info("circulator: copy found with an unlimited circ duration");
1363 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1364 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1365 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1366 $circ->renewal_remaining(0);
1369 $circ->target_copy( $copy->id );
1370 $circ->usr( $patron->id );
1371 $circ->circ_lib( $self->circ_lib );
1373 if( $self->is_renewal ) {
1374 $circ->opac_renewal('t') if $self->opac_renewal;
1375 $circ->phone_renewal('t') if $self->phone_renewal;
1376 $circ->desk_renewal('t') if $self->desk_renewal;
1377 $circ->renewal_remaining($self->renewal_remaining);
1378 $circ->circ_staff($self->editor->requestor->id);
1382 # if the user provided an overiding checkout time,
1383 # (e.g. the checkout really happened several hours ago), then
1384 # we apply that here. Does this need a perm??
1385 $circ->xact_start(clense_ISO8601($self->checkout_time))
1386 if $self->checkout_time;
1388 # if a patron is renewing, 'requestor' will be the patron
1389 $circ->circ_staff($self->editor->requestor->id);
1390 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1396 sub apply_modified_due_date {
1398 my $circ = $self->circ;
1399 my $copy = $self->copy;
1401 if( $self->due_date ) {
1403 return $self->bail_on_events($self->editor->event)
1404 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1406 $circ->due_date(clense_ISO8601($self->due_date));
1410 # if the due_date lands on a day when the location is closed
1411 return unless $copy and $circ->due_date;
1413 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1415 # due-date overlap should be determined by the location the item
1416 # is checked out from, not the owning or circ lib of the item
1417 my $org = $self->editor->requestor->ws_ou;
1419 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1420 " with an item due date of ".$circ->due_date );
1422 my $dateinfo = $U->storagereq(
1423 'open-ils.storage.actor.org_unit.closed_date.overlap',
1424 $org, $circ->due_date );
1427 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1428 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1430 # XXX make the behavior more dynamic
1431 # for now, we just push the due date to after the close date
1432 $circ->due_date($dateinfo->{end});
1439 sub create_due_date {
1440 my( $self, $duration ) = @_;
1441 # if there is a raw time component (e.g. from postgres),
1442 # turn it into an interval that interval_to_seconds can parse
1443 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1444 my ($sec,$min,$hour,$mday,$mon,$year) =
1445 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1446 $year += 1900; $mon += 1;
1447 my $due_date = sprintf(
1448 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1449 $year, $mon, $mday, $hour, $min, $sec);
1455 sub make_precat_copy {
1457 my $copy = $self->copy;
1460 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1462 $copy->editor($self->editor->requestor->id);
1463 $copy->edit_date('now');
1464 $copy->dummy_title($self->dummy_title);
1465 $copy->dummy_author($self->dummy_author);
1467 $self->update_copy();
1471 $logger->info("circulator: Creating a new precataloged ".
1472 "copy in checkout with barcode " . $self->copy_barcode);
1474 $copy = Fieldmapper::asset::copy->new;
1475 $copy->circ_lib($self->circ_lib);
1476 $copy->creator($self->editor->requestor->id);
1477 $copy->editor($self->editor->requestor->id);
1478 $copy->barcode($self->copy_barcode);
1479 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1480 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1481 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1483 $copy->dummy_title($self->dummy_title || "");
1484 $copy->dummy_author($self->dummy_author || "");
1486 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1488 $self->push_events($self->editor->event);
1492 # this is a little bit of a hack, but we need to
1493 # get the copy into the script runner
1494 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1498 sub checkout_noncat {
1504 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1505 my $count = $self->noncat_count || 1;
1506 my $cotime = clense_ISO8601($self->checkout_time) || "";
1508 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1512 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1513 $self->editor->requestor->id,
1521 $self->push_events($evt);
1532 $self->log_me("do_checkin()");
1535 return $self->bail_on_events(
1536 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1539 if( $self->checkin_check_holds_shelf() ) {
1540 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1541 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1542 $self->checkin_flesh_events;
1546 unless( $self->is_renewal ) {
1547 return $self->bail_on_events($self->editor->event)
1548 unless $self->editor->allowed('COPY_CHECKIN');
1551 $self->push_events($self->check_copy_alert());
1552 $self->push_events($self->check_checkin_copy_status());
1554 # the renew code will have already found our circulation object
1555 unless( $self->is_renewal and $self->circ ) {
1556 my $circs = $self->editor->search_action_circulation(
1557 { target_copy => $self->copy->id, checkin_time => undef });
1558 $self->circ($$circs[0]);
1560 # for now, just warn if there are multiple open circs on a copy
1561 $logger->warn("circulator: we have ".scalar(@$circs).
1562 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1565 # if the circ is marked as 'claims returned', add the event to the list
1566 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1567 if ($self->circ and $self->circ->stop_fines
1568 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1570 # handle the overridable events
1571 $self->override_events unless $self->is_renewal;
1572 return if $self->bail_out;
1576 $self->editor->search_action_transit_copy(
1577 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1581 $self->checkin_handle_circ;
1582 return if $self->bail_out;
1583 $self->checkin_changed(1);
1585 } elsif( $self->transit ) {
1586 my $hold_transit = $self->process_received_transit;
1587 $self->checkin_changed(1);
1589 if( $self->bail_out ) {
1590 $self->checkin_flesh_events;
1594 if( my $e = $self->check_checkin_copy_status() ) {
1595 # If the original copy status is special, alert the caller
1596 my $ev = $self->events;
1597 $self->events([$e]);
1598 $self->override_events;
1599 return if $self->bail_out;
1603 if( $hold_transit or
1604 $U->copy_status($self->copy->status)->id
1605 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1608 if( $hold_transit ) {
1609 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1611 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1616 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1618 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1619 $self->reshelve_copy(1);
1620 $self->cancelled_hold_transit(1);
1621 $self->notify_hold(0); # don't notify for cancelled holds
1622 return if $self->bail_out;
1626 # hold transited to correct location
1627 $self->checkin_flesh_events;
1632 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1634 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1635 " that is in-transit, but there is no transit.. repairing");
1636 $self->reshelve_copy(1);
1637 return if $self->bail_out;
1640 if( $self->is_renewal ) {
1641 $self->push_events(OpenILS::Event->new('SUCCESS'));
1645 # ------------------------------------------------------------------------------
1646 # Circulations and transits are now closed where necessary. Now go on to see if
1647 # this copy can fulfill a hold or needs to be routed to a different location
1648 # ------------------------------------------------------------------------------
1650 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1651 return if $self->bail_out;
1653 } else { # not needed for a hold
1655 my $circ_lib = (ref $self->copy->circ_lib) ?
1656 $self->copy->circ_lib->id : $self->copy->circ_lib;
1658 if( $self->remote_hold ) {
1659 $circ_lib = $self->remote_hold->pickup_lib;
1660 $logger->warn("circulator: Copy ".$self->copy->barcode.
1661 " is on a remote hold's shelf, sending to $circ_lib");
1664 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1666 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1668 $self->checkin_handle_precat();
1669 return if $self->bail_out;
1673 my $bc = $self->copy->barcode;
1674 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1675 $self->checkin_build_copy_transit($circ_lib);
1676 return if $self->bail_out;
1677 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1681 $self->reshelve_copy;
1682 return if $self->bail_out;
1684 unless($self->checkin_changed) {
1686 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1687 my $stat = $U->copy_status($self->copy->status)->id;
1689 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1690 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1691 $self->bail_out(1); # no need to commit anything
1695 $self->push_events(OpenILS::Event->new('SUCCESS'))
1696 unless @{$self->events};
1700 # ------------------------------------------------------------------------------
1701 # Update the patron penalty info in the DB
1702 # ------------------------------------------------------------------------------
1703 $U->update_patron_penalties(
1704 authtoken => $self->editor->authtoken,
1705 patron => $self->patron,
1706 background => 1 ) if $self->is_checkin;
1708 $self->checkin_flesh_events;
1714 my $force = $self->force || shift;
1715 my $copy = $self->copy;
1717 my $stat = $U->copy_status($copy->status)->id;
1720 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1721 $stat != OILS_COPY_STATUS_CATALOGING and
1722 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1723 $stat != OILS_COPY_STATUS_RESHELVING )) {
1725 $copy->status( OILS_COPY_STATUS_RESHELVING );
1727 $self->checkin_changed(1);
1732 # Returns true if the item is at the current location
1733 # because it was transited there for a hold and the
1734 # hold has not been fulfilled
1735 sub checkin_check_holds_shelf {
1737 return 0 unless $self->copy;
1740 $U->copy_status($self->copy->status)->id ==
1741 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1743 # find the hold that put us on the holds shelf
1744 my $holds = $self->editor->search_action_hold_request(
1746 current_copy => $self->copy->id,
1747 capture_time => { '!=' => undef },
1748 fulfillment_time => undef,
1749 cancel_time => undef,
1754 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1755 $self->reshelve_copy(1);
1759 my $hold = $$holds[0];
1761 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1762 $hold->id. "] for copy ".$self->copy->barcode);
1764 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1765 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1769 $logger->info("circulator: hold is not for here..");
1770 $self->remote_hold($hold);
1775 sub checkin_handle_precat {
1777 my $copy = $self->copy;
1779 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1780 $copy->status(OILS_COPY_STATUS_CATALOGING);
1781 $self->update_copy();
1782 $self->checkin_changed(1);
1783 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1788 sub checkin_build_copy_transit {
1791 my $copy = $self->copy;
1792 my $transit = Fieldmapper::action::transit_copy->new;
1794 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1795 $logger->info("circulator: transiting copy to $dest");
1797 $transit->source($self->editor->requestor->ws_ou);
1798 $transit->dest($dest);
1799 $transit->target_copy($copy->id);
1800 $transit->source_send_time('now');
1801 $transit->copy_status( $U->copy_status($copy->status)->id );
1803 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1805 return $self->bail_on_events($self->editor->event)
1806 unless $self->editor->create_action_transit_copy($transit);
1808 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1810 $self->checkin_changed(1);
1814 sub attempt_checkin_hold_capture {
1816 my $copy = $self->copy;
1818 # See if this copy can fulfill any holds
1819 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1820 $self->editor, $copy, $self->editor->requestor );
1823 $logger->debug("circulator: no potential permitted".
1824 "holds found for copy ".$copy->barcode);
1828 $self->retarget($retarget);
1830 $logger->info("circulator: found permitted hold ".
1831 $hold->id . " for copy, capturing...");
1833 $hold->current_copy($copy->id);
1834 $hold->capture_time('now');
1836 # prevent DB errors caused by fetching
1837 # holds from storage, and updating through cstore
1838 $hold->clear_fulfillment_time;
1839 $hold->clear_fulfillment_staff;
1840 $hold->clear_fulfillment_lib;
1841 $hold->clear_expire_time;
1842 $hold->clear_cancel_time;
1843 $hold->clear_prev_check_time unless $hold->prev_check_time;
1845 $self->bail_on_events($self->editor->event)
1846 unless $self->editor->update_action_hold_request($hold);
1848 $self->checkin_changed(1);
1850 return 1 if $self->bail_out;
1852 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1854 # This hold was captured in the correct location
1855 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1856 $self->push_events(OpenILS::Event->new('SUCCESS'));
1858 #$self->do_hold_notify($hold->id);
1859 $self->notify_hold($hold->id);
1863 # Hold needs to be picked up elsewhere. Build a hold
1864 # transit and route the item.
1865 $self->checkin_build_hold_transit();
1866 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1867 return 1 if $self->bail_out;
1869 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1872 # make sure we save the copy status
1877 sub do_hold_notify {
1878 my( $self, $holdid ) = @_;
1880 $logger->info("circulator: running delayed hold notify process");
1882 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1883 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1885 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1886 hold_id => $holdid, requestor => $self->editor->requestor);
1888 $logger->debug("circulator: built hold notifier");
1890 if(!$notifier->event) {
1892 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1894 my $stat = $notifier->send_email_notify;
1895 if( $stat == '1' ) {
1896 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1900 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1903 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1907 sub retarget_holds {
1908 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1909 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1910 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1911 # no reason to wait for the return value
1915 sub checkin_build_hold_transit {
1918 my $copy = $self->copy;
1919 my $hold = $self->hold;
1920 my $trans = Fieldmapper::action::hold_transit_copy->new;
1922 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1924 $trans->hold($hold->id);
1925 $trans->source($self->editor->requestor->ws_ou);
1926 $trans->dest($hold->pickup_lib);
1927 $trans->source_send_time("now");
1928 $trans->target_copy($copy->id);
1930 # when the copy gets to its destination, it will recover
1931 # this status - put it onto the holds shelf
1932 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1934 return $self->bail_on_events($self->editor->event)
1935 unless $self->editor->create_action_hold_transit_copy($trans);
1940 sub process_received_transit {
1942 my $copy = $self->copy;
1943 my $copyid = $self->copy->id;
1945 my $status_name = $U->copy_status($copy->status)->name;
1946 $logger->debug("circulator: attempting transit receive on ".
1947 "copy $copyid. Copy status is $status_name");
1949 my $transit = $self->transit;
1951 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1952 # - this item is in-transit to a different location
1954 my $tid = $transit->id;
1955 my $loc = $self->editor->requestor->ws_ou;
1956 my $dest = $transit->dest;
1958 $logger->info("circulator: Fowarding transit on copy which is destined ".
1959 "for a different location. transit=$tid, copy=$copyid, current ".
1960 "location=$loc, destination location=$dest");
1962 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1964 # grab the associated hold object if available
1965 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1966 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1968 return $self->bail_on_events($evt);
1971 # The transit is received, set the receive time
1972 $transit->dest_recv_time('now');
1973 $self->bail_on_events($self->editor->event)
1974 unless $self->editor->update_action_transit_copy($transit);
1976 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1978 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1979 $copy->status( $transit->copy_status );
1980 $self->update_copy();
1981 return if $self->bail_out;
1985 #$self->do_hold_notify($hold_transit->hold);
1986 $self->notify_hold($hold_transit->hold);
1991 OpenILS::Event->new(
1994 payload => { transit => $transit, holdtransit => $hold_transit } ));
1996 return $hold_transit;
2000 sub checkin_handle_circ {
2004 my $circ = $self->circ;
2005 my $copy = $self->copy;
2009 # backdate the circ if necessary
2010 if($self->backdate) {
2011 $self->checkin_handle_backdate;
2012 return if $self->bail_out;
2015 if(!$circ->stop_fines) {
2016 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2017 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2018 $circ->stop_fines_time('now') unless $self->backdate;
2019 $circ->stop_fines_time($self->backdate) if $self->backdate;
2022 # see if there are any fines owed on this circ. if not, close it
2023 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2024 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2026 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2028 # Set the checkin vars since we have the item
2029 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2031 $circ->checkin_staff($self->editor->requestor->id);
2032 $circ->checkin_lib($self->editor->requestor->ws_ou);
2034 my $circ_lib = (ref $self->copy->circ_lib) ?
2035 $self->copy->circ_lib->id : $self->copy->circ_lib;
2036 my $stat = $U->copy_status($self->copy->status)->id;
2038 # If the item is lost/missing and it needs to be sent home, don't
2039 # reshelve the copy, leave it lost/missing so the recipient will know
2040 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2041 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2042 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2045 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2049 return $self->bail_on_events($self->editor->event)
2050 unless $self->editor->update_action_circulation($circ);
2054 sub checkin_handle_backdate {
2057 my $bd = $self->backdate;
2059 # ------------------------------------------------------------------
2060 # clean up the backdate for date comparison
2061 # we want any bills created on or after the backdate
2062 # ------------------------------------------------------------------
2063 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2064 #$bd = "${bd}T23:59:59";
2066 my $bills = $self->editor->search_money_billing(
2068 billing_ts => { '>=' => $bd },
2069 xact => $self->circ->id,
2070 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2074 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2076 for my $bill (@$bills) {
2077 unless( $U->is_true($bill->voided) ) {
2078 $logger->info("backdate voiding bill ".$bill->id);
2080 $bill->void_time('now');
2081 $bill->voider($self->editor->requestor->id);
2082 my $n = $bill->note || "";
2083 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2085 $self->bail_on_events($self->editor->event)
2086 unless $self->editor->update_money_billing($bill);
2094 # XXX Legacy version for Circ.pm support
2095 sub _checkin_handle_backdate {
2096 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
2099 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2100 $bd = "${bd}T23:59:59";
2102 my $bills = $session->request(
2103 "open-ils.storage.direct.money.billing.search_where.atomic",
2104 billing_ts => { '>=' => $bd },
2106 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2109 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2112 for my $bill (@$bills) {
2113 unless( $U->is_true($bill->voided) ) {
2114 $logger->debug("voiding bill ".$bill->id);
2116 $bill->void_time('now');
2117 $bill->voider($requestor->id);
2118 my $n = $bill->note || "";
2119 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
2120 my $s = $session->request(
2121 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
2122 return $U->DB_UPDATE_FAILED($bill) unless $s;
2136 sub find_patron_from_copy {
2138 my $circs = $self->editor->search_action_circulation(
2139 { target_copy => $self->copy->id, checkin_time => undef });
2140 my $circ = $circs->[0];
2141 return unless $circ;
2142 my $u = $self->editor->retrieve_actor_user($circ->usr)
2143 or return $self->bail_on_events($self->editor->event);
2147 sub check_checkin_copy_status {
2149 my $copy = $self->copy;
2155 my $status = $U->copy_status($copy->status)->id;
2158 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2159 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2160 $status == OILS_COPY_STATUS_IN_PROCESS ||
2161 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2162 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2163 $status == OILS_COPY_STATUS_CATALOGING ||
2164 $status == OILS_COPY_STATUS_RESHELVING );
2166 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2167 if( $status == OILS_COPY_STATUS_LOST );
2169 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2170 if( $status == OILS_COPY_STATUS_MISSING );
2172 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2177 # --------------------------------------------------------------------------
2178 # On checkin, we need to return as many relevant objects as we can
2179 # --------------------------------------------------------------------------
2180 sub checkin_flesh_events {
2183 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2184 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2185 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2189 for my $evt (@{$self->events}) {
2192 $payload->{copy} = $U->unflesh_copy($self->copy);
2193 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2194 $payload->{circ} = $self->circ;
2195 $payload->{transit} = $self->transit;
2196 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2198 # $self->hold may or may not have been replaced with a
2199 # valid hold after processing a cancelled hold
2200 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2202 $evt->{payload} = $payload;
2207 my( $self, $msg ) = @_;
2208 my $bc = ($self->copy) ? $self->copy->barcode :
2211 my $usr = ($self->patron) ? $self->patron->id : "";
2212 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2213 ", recipient=$usr, copy=$bc");
2219 $self->log_me("do_renew()");
2220 $self->is_renewal(1);
2222 # Make sure there is an open circ to renew that is not
2223 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2224 my $circ = $self->editor->search_action_circulation(
2225 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2228 $circ = $self->editor->search_action_circulation(
2230 target_copy => $self->copy->id,
2231 stop_fines => OILS_STOP_FINES_MAX_FINES,
2232 checkin_time => undef
2237 return $self->bail_on_events($self->editor->event) unless $circ;
2239 # A user is not allowed to renew another user's items without permission
2240 unless( $circ->usr eq $self->editor->requestor->id ) {
2241 return $self->bail_on_events($self->editor->events)
2242 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2245 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2246 if $circ->renewal_remaining < 1;
2248 # -----------------------------------------------------------------
2250 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2253 $self->run_renew_permit;
2256 $self->do_checkin();
2257 return if $self->bail_out;
2259 unless( $self->permit_override ) {
2261 return if $self->bail_out;
2262 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2263 $self->remove_event('ITEM_NOT_CATALOGED');
2266 $self->override_events;
2267 return if $self->bail_out;
2270 $self->do_checkout();
2275 my( $self, $evt ) = @_;
2276 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2277 $logger->debug("circulator: removing event from list: $evt");
2278 my @events = @{$self->events};
2279 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2284 my( $self, $evt ) = @_;
2285 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2286 return grep { $_->{textcode} eq $evt } @{$self->events};
2291 sub run_renew_permit {
2296 if(!$self->legacy_script_support) {
2297 my $results = $self->run_indb_circ_test;
2298 unless($self->circ_test_success) {
2299 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2304 my $runner = $self->script_runner;
2306 $runner->load($self->circ_permit_renew);
2307 my $result = $runner->run or
2308 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2309 my $events = $result->{events};
2312 $logger->activity("ciculator: circ_permit_renew for user ".
2313 $self->patron->id." returned events: @$events") if @$events;
2315 $self->push_events(OpenILS::Event->new($_)) for @$events;
2317 $logger->debug("circulator: re-creating script runner to be safe");
2318 #$self->mk_script_runner;