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];
511 $self->volume($copy->call_number);
512 $self->title($self->volume->record);
513 $self->copy->call_number($self->volume->id);
514 $self->volume->record($self->title->id);
515 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
517 # We can't renew if there is no copy
518 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
519 if $self->is_renewal;
524 return undef if $self->is_checkin;
526 # --------------------------------------------------------------------------
528 # --------------------------------------------------------------------------
530 if( $self->patron_id ) {
531 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
533 } elsif( $self->patron_barcode ) {
535 my $card = $e->search_actor_card(
536 {barcode => $self->patron_barcode})->[0] or return $e->event;
538 $patron = $e->search_actor_user(
539 {card => $card->id})->[0] or return $e->event;
542 if( my $copy = $self->copy ) {
543 my $circs = $e->search_action_circulation(
544 {target_copy => $copy->id, checkin_time => undef});
546 if( my $circ = $circs->[0] ) {
547 $patron = $e->retrieve_actor_user($circ->usr)
553 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
554 unless $self->patron($patron);
557 # --------------------------------------------------------------------------
558 # This builds the script runner environment and fetches most of the
560 # --------------------------------------------------------------------------
561 sub mk_script_runner {
567 qw/copy copy_barcode copy_id patron
568 patron_id patron_barcode volume title editor/;
570 # Translate our objects into the ScriptBuilder args hash
571 $$args{$_} = $self->$_() for @fields;
573 $args->{ignore_user_status} = 1 if $self->is_checkin;
574 $$args{fetch_patron_by_circ_copy} = 1;
575 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
577 if( my $pco = $self->pending_checkouts ) {
578 $logger->info("circulator: we were given a pending checkouts number of $pco");
579 $$args{patronItemsOut} = $pco;
582 # This fetches most of the objects we need
583 $self->script_runner(
584 OpenILS::Application::Circ::ScriptBuilder->build($args));
586 # Now we translate the ScriptBuilder objects back into self
587 $self->$_($$args{$_}) for @fields;
589 my @evts = @{$args->{_events}} if $args->{_events};
591 $logger->debug("circulator: script builder returned events: @evts") if @evts;
595 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
596 if(!$self->is_noncat and
598 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
602 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
603 return $self->bail_on_events(@e);
607 $self->is_precat(1) if $self->copy
608 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
610 # We can't renew if there is no copy
611 return $self->bail_on_events(@evts) if
612 $self->is_renewal and !$self->copy;
614 # Set some circ-specific flags in the script environment
615 my $evt = "environment";
616 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
618 if( $self->is_noncat ) {
619 $self->script_runner->insert("$evt.isNonCat", 1);
620 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
623 if( $self->is_precat ) {
624 $self->script_runner->insert("environment.isPrecat", 1, 1);
627 $self->script_runner->add_path( $_ ) for @$script_libs;
632 # --------------------------------------------------------------------------
633 # Does the circ permit work
634 # --------------------------------------------------------------------------
638 $self->log_me("do_permit()");
640 unless( $self->editor->requestor->id == $self->patron->id ) {
641 return $self->bail_on_events($self->editor->event)
642 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
645 $self->check_captured_holds();
646 $self->do_copy_checks();
647 return if $self->bail_out;
648 $self->run_patron_permit_scripts();
649 $self->run_copy_permit_scripts()
650 unless $self->is_precat or $self->is_noncat;
651 $self->override_events() unless
652 $self->is_renewal and not $self->check_penalty_on_renew;
653 return if $self->bail_out;
655 if( $self->is_precat ) {
658 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
659 return $self->bail_out(1) unless $self->is_renewal;
665 payload => $self->mk_permit_key));
669 sub check_captured_holds {
671 my $copy = $self->copy;
672 my $patron = $self->patron;
674 return undef unless $copy;
676 my $s = $U->copy_status($copy->status)->id;
677 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
678 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
680 # Item is on the holds shelf, make sure it's going to the right person
681 my $holds = $self->editor->search_action_hold_request(
684 current_copy => $copy->id ,
685 capture_time => { '!=' => undef },
686 cancel_time => undef,
687 fulfillment_time => undef
693 if( $holds and $$holds[0] ) {
694 return undef if $$holds[0]->usr == $patron->id;
697 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
699 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
705 my $copy = $self->copy;
708 my $stat = $U->copy_status($copy->status)->id;
710 # We cannot check out a copy if it is in-transit
711 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
712 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
715 $self->handle_claims_returned();
716 return if $self->bail_out;
718 # no claims returned circ was found, check if there is any open circ
719 unless( $self->is_renewal ) {
720 my $circs = $self->editor->search_action_circulation(
721 { target_copy => $copy->id, checkin_time => undef }
724 return $self->bail_on_events(
725 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
730 sub send_penalty_request {
732 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
733 $self->penalty_request(
735 'open-ils.penalty.patron_penalty.calculate',
737 authtoken => $self->editor->authtoken,
738 patron => $self->patron } ) );
741 sub gather_penalty_request {
743 return [] unless $self->penalty_request;
744 my $data = $self->penalty_request->recv;
746 throw $data if UNIVERSAL::isa($data,'Error');
747 $data = $data->content;
748 return $data->{fatal_penalties};
750 $logger->error("circulator: penalty request returned no data");
754 my $LEGACY_CIRC_EVENT_MAP = {
755 'actor.usr.barred' => 'PATRON_BARRED',
756 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
757 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
758 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
759 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
760 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
761 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
762 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
763 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
767 # ---------------------------------------------------------------------
768 # This pushes any patron-related events into the list but does not
769 # set bail_out for any events
770 # ---------------------------------------------------------------------
771 sub run_patron_permit_scripts {
773 my $runner = $self->script_runner;
774 my $patronid = $self->patron->id;
778 if(!$self->legacy_script_support) {
780 my $results = $self->run_indb_circ_test;
781 unless($self->circ_test_success) {
782 push(@allevents, OpenILS::Event->new(
783 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
788 $self->send_penalty_request() unless
789 $self->is_renewal and not $self->check_penalty_on_renew;
792 # --------------------------------------------------------------------- # Now run the patron permit script
793 # ---------------------------------------------------------------------
794 $runner->load($self->circ_permit_patron);
795 my $result = $runner->run or
796 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
798 my $patron_events = $result->{events};
800 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
801 [] : $self->gather_penalty_request();
803 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
806 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
808 $self->push_events(@allevents);
811 sub run_indb_circ_test {
813 return $self->matrix_test_result if $self->matrix_test_result;
815 my $dbfunc = ($self->is_renewal) ?
816 'action.item_user_renew_test' : 'action.item_user_circ_test';
818 my $results = ($self->matrix_test_result) ?
819 $self->matrix_test_result :
820 $self->editor->json_query(
823 $self->editor->requestor->ws_ou,
824 ($self->is_precat) ? undef : $self->copy->id,
830 $self->circ_test_success($U->is_true($results->[0]->{success}));
831 if($self->circ_test_success) {
832 $self->circ_matrix_test(
833 $self->editor->retrieve_config_circ_matrix_test(
834 $results->[0]->{matchpoint}
839 if($self->circ_test_success) {
840 $self->circ_matrix_ruleset(
841 $self->editor->retrieve_config_circ_matrix_ruleset([
842 $results->[0]->{matchpoint},
845 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
853 return $self->matrix_test_result($results);
858 $self->run_indb_circ_test;
861 circ_test_success => $self->circ_test_success,
862 failure_events => [],
866 unless($self->circ_test_success) {
867 push(@{$results->{failure_codes}},
868 $_->{fail_part}) for @{$self->matrix_test_result};
869 push(@{$results->{failure_events}},
870 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
871 for @{$self->matrix_test_result};
875 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
876 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
877 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
879 my $policy = $self->get_circ_policy(
880 $duration_rule, $recurring_fine_rule, $max_fine_rule);
882 $$results{$_} = $$policy{$_} for keys %$policy;
886 # ---------------------------------------------------------------------
887 # Loads the circ policy info for duration, recurring fine, and max
888 # fine based on the current copy
889 # ---------------------------------------------------------------------
890 sub get_circ_policy {
891 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
894 duration_rule => $duration_rule->name,
895 recurring_fine_rule => $recurring_fine_rule->name,
896 max_fine_rule => $max_fine_rule->name,
897 max_fine => $self->get_max_fine_amount($max_fine_rule),
898 fine_interval => $recurring_fine_rule->recurance_interval,
899 renewal_remaining => $duration_rule->max_renewals
902 $policy->{duration} = $duration_rule->shrt
903 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
904 $policy->{duration} = $duration_rule->normal
905 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
906 $policy->{duration} = $duration_rule->extended
907 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
909 $policy->{recurring_fine} = $recurring_fine_rule->low
910 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
911 $policy->{recurring_fine} = $recurring_fine_rule->normal
912 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
913 $policy->{recurring_fine} = $recurring_fine_rule->high
914 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
919 sub get_max_fine_amount {
921 my $max_fine_rule = shift;
922 my $max_amount = $max_fine_rule->amount;
924 # if is_percent is true then the max->amount is
925 # use as a percentage of the copy price
926 if ($U->is_true($max_fine_rule->is_percent)) {
928 my $ol = ($self->is_precat) ?
929 $self->editor->requestor->ws_ou : $self->volume->owning_lib;
931 my $default_price = $U->ou_ancestor_setting_value(
932 $ol, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
933 my $charge_on_0 = $U->ou_ancestor_setting_value(
934 $ol, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
936 # Find the most appropriate "price" -- same definition as the
937 # LOST price. See OpenILS::Circ::new_set_circ_lost
938 $max_amount = $self->copy->price;
939 $max_amount = $default_price unless defined $max_amount;
940 $max_amount = 0 if $max_amount < 0;
941 $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
943 $max_amount *= $max_fine_rule->amount / 100;
951 sub run_copy_permit_scripts {
953 my $copy = $self->copy || return;
954 my $runner = $self->script_runner;
958 if(!$self->legacy_script_support) {
959 my $results = $self->run_indb_circ_test;
960 unless($self->circ_test_success) {
961 push(@allevents, OpenILS::Event->new(
962 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
966 # ---------------------------------------------------------------------
967 # Capture all of the copy permit events
968 # ---------------------------------------------------------------------
969 $runner->load($self->circ_permit_copy);
970 my $result = $runner->run or
971 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
972 my $copy_events = $result->{events};
974 # ---------------------------------------------------------------------
975 # Now collect all of the events together
976 # ---------------------------------------------------------------------
977 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
980 # See if this copy has an alert message
981 my $ae = $self->check_copy_alert();
982 push( @allevents, $ae ) if $ae;
984 # uniquify the events
985 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
986 @allevents = values %hash;
989 $_->{payload} = $copy if
990 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
993 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
995 $self->push_events(@allevents);
999 sub check_copy_alert {
1001 return undef if $self->is_renewal;
1002 return OpenILS::Event->new(
1003 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1004 if $self->copy and $self->copy->alert_message;
1010 # --------------------------------------------------------------------------
1011 # If the call is overriding and has permissions to override every collected
1012 # event, the are cleared. Any event that the caller does not have
1013 # permission to override, will be left in the event list and bail_out will
1015 # XXX We need code in here to cancel any holds/transits on copies
1016 # that are being force-checked out
1017 # --------------------------------------------------------------------------
1018 sub override_events {
1020 my @events = @{$self->events};
1021 return unless @events;
1023 if(!$self->override) {
1024 return $self->bail_out(1)
1025 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1030 for my $e (@events) {
1031 my $tc = $e->{textcode};
1032 next if $tc eq 'SUCCESS';
1033 my $ov = "$tc.override";
1034 $logger->info("circulator: attempting to override event: $ov");
1036 return $self->bail_on_events($self->editor->event)
1037 unless( $self->editor->allowed($ov) );
1042 # --------------------------------------------------------------------------
1043 # If there is an open claimsreturn circ on the requested copy, close the
1044 # circ if overriding, otherwise bail out
1045 # --------------------------------------------------------------------------
1046 sub handle_claims_returned {
1048 my $copy = $self->copy;
1050 my $CR = $self->editor->search_action_circulation(
1052 target_copy => $copy->id,
1053 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1054 checkin_time => undef,
1058 return unless ($CR = $CR->[0]);
1062 # - If the caller has set the override flag, we will check the item in
1063 if($self->override) {
1065 $CR->checkin_time('now');
1066 $CR->checkin_lib($self->editor->requestor->ws_ou);
1067 $CR->checkin_staff($self->editor->requestor->id);
1069 $evt = $self->editor->event
1070 unless $self->editor->update_action_circulation($CR);
1073 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1076 $self->bail_on_events($evt) if $evt;
1081 # --------------------------------------------------------------------------
1082 # This performs the checkout
1083 # --------------------------------------------------------------------------
1087 $self->log_me("do_checkout()");
1089 # make sure perms are good if this isn't a renewal
1090 unless( $self->is_renewal ) {
1091 return $self->bail_on_events($self->editor->event)
1092 unless( $self->editor->allowed('COPY_CHECKOUT') );
1095 # verify the permit key
1096 unless( $self->check_permit_key ) {
1097 if( $self->permit_override ) {
1098 return $self->bail_on_events($self->editor->event)
1099 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1101 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1105 # if this is a non-cataloged circ, build the circ and finish
1106 if( $self->is_noncat ) {
1107 $self->checkout_noncat;
1109 OpenILS::Event->new('SUCCESS',
1110 payload => { noncat_circ => $self->circ }));
1114 if( $self->is_precat ) {
1115 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1116 $self->make_precat_copy;
1117 return if $self->bail_out;
1119 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1120 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1123 $self->do_copy_checks;
1124 return if $self->bail_out;
1126 $self->run_checkout_scripts();
1127 return if $self->bail_out;
1129 $self->build_checkout_circ_object();
1130 return if $self->bail_out;
1132 $self->apply_modified_due_date();
1133 return if $self->bail_out;
1135 return $self->bail_on_events($self->editor->event)
1136 unless $self->editor->create_action_circulation($self->circ);
1138 # refresh the circ to force local time zone for now
1139 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1141 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1143 return if $self->bail_out;
1145 $self->handle_checkout_holds();
1146 return if $self->bail_out;
1148 # ------------------------------------------------------------------------------
1149 # Update the patron penalty info in the DB. Run it for permit-overrides or
1150 # renewals since both of those cases do not require the penalty server to
1151 # run during the permit phase of the checkout
1152 # ------------------------------------------------------------------------------
1153 if( $self->permit_override or $self->is_renewal ) {
1154 $U->update_patron_penalties(
1155 authtoken => $self->editor->authtoken,
1156 patron => $self->patron,
1161 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1163 OpenILS::Event->new('SUCCESS',
1165 copy => $U->unflesh_copy($self->copy),
1166 circ => $self->circ,
1168 holds_fulfilled => $self->fulfilled_holds,
1176 my $copy = $self->copy;
1178 my $stat = $copy->status if ref $copy->status;
1179 my $loc = $copy->location if ref $copy->location;
1180 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1182 $copy->status($stat->id) if $stat;
1183 $copy->location($loc->id) if $loc;
1184 $copy->circ_lib($circ_lib->id) if $circ_lib;
1185 $copy->editor($self->editor->requestor->id);
1186 $copy->edit_date('now');
1187 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1189 return $self->bail_on_events($self->editor->event)
1190 unless $self->editor->update_asset_copy($self->copy);
1192 $copy->status($U->copy_status($copy->status));
1193 $copy->location($loc) if $loc;
1194 $copy->circ_lib($circ_lib) if $circ_lib;
1198 sub bail_on_events {
1199 my( $self, @evts ) = @_;
1200 $self->push_events(@evts);
1204 sub handle_checkout_holds {
1207 my $copy = $self->copy;
1208 my $patron = $self->patron;
1210 my $holds = $self->editor->search_action_hold_request(
1212 current_copy => $copy->id ,
1213 cancel_time => undef,
1214 fulfillment_time => undef
1220 # XXX We should only fulfill one hold here...
1221 # XXX If a hold was transited to the user who is checking out
1222 # the item, we need to make sure that hold is what's grabbed
1225 # for now, just sort by id to get what should be the oldest hold
1226 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1227 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1228 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1231 my $hold = $myholds[0];
1233 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1235 # if the hold was never officially captured, capture it.
1236 $hold->capture_time('now') unless $hold->capture_time;
1238 # just make sure it's set correctly
1239 $hold->current_copy($copy->id);
1241 $hold->fulfillment_time('now');
1242 $hold->fulfillment_staff($self->editor->requestor->id);
1243 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1245 return $self->bail_on_events($self->editor->event)
1246 unless $self->editor->update_action_hold_request($hold);
1248 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1250 push( @fulfilled, $hold->id );
1253 # If there are any holds placed for other users that point to this copy,
1254 # then we need to un-target those holds so the targeter can pick a new copy
1257 $logger->info("circulator: un-targeting hold ".$_->id.
1258 " because copy ".$copy->id." is getting checked out");
1260 # - make the targeter process this hold at next run
1261 $_->clear_prev_check_time;
1263 # - clear out the targetted copy
1264 $_->clear_current_copy;
1265 $_->clear_capture_time;
1267 return $self->bail_on_event($self->editor->event)
1268 unless $self->editor->update_action_hold_request($_);
1272 $self->fulfilled_holds(\@fulfilled);
1277 sub run_checkout_scripts {
1281 my $runner = $self->script_runner;
1290 if(!$self->legacy_script_support) {
1291 $self->run_indb_circ_test();
1292 $duration = $self->circ_matrix_ruleset->duration_rule;
1293 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1294 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1298 $runner->load($self->circ_duration);
1300 my $result = $runner->run or
1301 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1303 $duration_name = $result->{durationRule};
1304 $recurring_name = $result->{recurringFinesRule};
1305 $max_fine_name = $result->{maxFine};
1308 $duration_name = $duration->name if $duration;
1309 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1312 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1313 return $self->bail_on_events($evt) if $evt;
1315 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1316 return $self->bail_on_events($evt) if $evt;
1318 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1319 return $self->bail_on_events($evt) if $evt;
1324 # The item circulates with an unlimited duration
1330 $self->duration_rule($duration);
1331 $self->recurring_fines_rule($recurring);
1332 $self->max_fine_rule($max_fine);
1336 sub build_checkout_circ_object {
1339 my $circ = Fieldmapper::action::circulation->new;
1340 my $duration = $self->duration_rule;
1341 my $max = $self->max_fine_rule;
1342 my $recurring = $self->recurring_fines_rule;
1343 my $copy = $self->copy;
1344 my $patron = $self->patron;
1348 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1350 my $dname = $duration->name;
1351 my $mname = $max->name;
1352 my $rname = $recurring->name;
1354 $logger->debug("circulator: building circulation ".
1355 "with duration=$dname, maxfine=$mname, recurring=$rname");
1357 $circ->duration($policy->{duration});
1358 $circ->recuring_fine($policy->{recurring_fine});
1359 $circ->duration_rule($duration->name);
1360 $circ->recuring_fine_rule($recurring->name);
1361 $circ->max_fine_rule($max->name);
1362 $circ->max_fine($policy->{max_fine});
1363 $circ->fine_interval($recurring->recurance_interval);
1364 $circ->renewal_remaining($duration->max_renewals);
1368 $logger->info("circulator: copy found with an unlimited circ duration");
1369 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1370 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1371 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1372 $circ->renewal_remaining(0);
1375 $circ->target_copy( $copy->id );
1376 $circ->usr( $patron->id );
1377 $circ->circ_lib( $self->circ_lib );
1379 if( $self->is_renewal ) {
1380 $circ->opac_renewal('t') if $self->opac_renewal;
1381 $circ->phone_renewal('t') if $self->phone_renewal;
1382 $circ->desk_renewal('t') if $self->desk_renewal;
1383 $circ->renewal_remaining($self->renewal_remaining);
1384 $circ->circ_staff($self->editor->requestor->id);
1388 # if the user provided an overiding checkout time,
1389 # (e.g. the checkout really happened several hours ago), then
1390 # we apply that here. Does this need a perm??
1391 $circ->xact_start(clense_ISO8601($self->checkout_time))
1392 if $self->checkout_time;
1394 # if a patron is renewing, 'requestor' will be the patron
1395 $circ->circ_staff($self->editor->requestor->id);
1396 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1402 sub apply_modified_due_date {
1404 my $circ = $self->circ;
1405 my $copy = $self->copy;
1407 if( $self->due_date ) {
1409 return $self->bail_on_events($self->editor->event)
1410 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1412 $circ->due_date(clense_ISO8601($self->due_date));
1416 # if the due_date lands on a day when the location is closed
1417 return unless $copy and $circ->due_date;
1419 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1421 # due-date overlap should be determined by the location the item
1422 # is checked out from, not the owning or circ lib of the item
1423 my $org = $self->editor->requestor->ws_ou;
1425 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1426 " with an item due date of ".$circ->due_date );
1428 my $dateinfo = $U->storagereq(
1429 'open-ils.storage.actor.org_unit.closed_date.overlap',
1430 $org, $circ->due_date );
1433 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1434 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1436 # XXX make the behavior more dynamic
1437 # for now, we just push the due date to after the close date
1438 $circ->due_date($dateinfo->{end});
1445 sub create_due_date {
1446 my( $self, $duration ) = @_;
1447 # if there is a raw time component (e.g. from postgres),
1448 # turn it into an interval that interval_to_seconds can parse
1449 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1450 my ($sec,$min,$hour,$mday,$mon,$year) =
1451 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1452 $year += 1900; $mon += 1;
1453 my $due_date = sprintf(
1454 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1455 $year, $mon, $mday, $hour, $min, $sec);
1461 sub make_precat_copy {
1463 my $copy = $self->copy;
1466 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1468 $copy->editor($self->editor->requestor->id);
1469 $copy->edit_date('now');
1470 $copy->dummy_title($self->dummy_title);
1471 $copy->dummy_author($self->dummy_author);
1473 $self->update_copy();
1477 $logger->info("circulator: Creating a new precataloged ".
1478 "copy in checkout with barcode " . $self->copy_barcode);
1480 $copy = Fieldmapper::asset::copy->new;
1481 $copy->circ_lib($self->circ_lib);
1482 $copy->creator($self->editor->requestor->id);
1483 $copy->editor($self->editor->requestor->id);
1484 $copy->barcode($self->copy_barcode);
1485 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1486 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1487 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1489 $copy->dummy_title($self->dummy_title || "");
1490 $copy->dummy_author($self->dummy_author || "");
1492 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1494 $self->push_events($self->editor->event);
1498 # this is a little bit of a hack, but we need to
1499 # get the copy into the script runner
1500 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1504 sub checkout_noncat {
1510 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1511 my $count = $self->noncat_count || 1;
1512 my $cotime = clense_ISO8601($self->checkout_time) || "";
1514 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1518 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1519 $self->editor->requestor->id,
1527 $self->push_events($evt);
1538 $self->log_me("do_checkin()");
1541 return $self->bail_on_events(
1542 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1545 if( $self->checkin_check_holds_shelf() ) {
1546 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1547 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1548 $self->checkin_flesh_events;
1552 unless( $self->is_renewal ) {
1553 return $self->bail_on_events($self->editor->event)
1554 unless $self->editor->allowed('COPY_CHECKIN');
1557 $self->push_events($self->check_copy_alert());
1558 $self->push_events($self->check_checkin_copy_status());
1560 # the renew code will have already found our circulation object
1561 unless( $self->is_renewal and $self->circ ) {
1562 my $circs = $self->editor->search_action_circulation(
1563 { target_copy => $self->copy->id, checkin_time => undef });
1564 $self->circ($$circs[0]);
1566 # for now, just warn if there are multiple open circs on a copy
1567 $logger->warn("circulator: we have ".scalar(@$circs).
1568 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1571 # if the circ is marked as 'claims returned', add the event to the list
1572 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1573 if ($self->circ and $self->circ->stop_fines
1574 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1576 # handle the overridable events
1577 $self->override_events unless $self->is_renewal;
1578 return if $self->bail_out;
1582 $self->editor->search_action_transit_copy(
1583 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1587 $self->checkin_handle_circ;
1588 return if $self->bail_out;
1589 $self->checkin_changed(1);
1591 } elsif( $self->transit ) {
1592 my $hold_transit = $self->process_received_transit;
1593 $self->checkin_changed(1);
1595 if( $self->bail_out ) {
1596 $self->checkin_flesh_events;
1600 if( my $e = $self->check_checkin_copy_status() ) {
1601 # If the original copy status is special, alert the caller
1602 my $ev = $self->events;
1603 $self->events([$e]);
1604 $self->override_events;
1605 return if $self->bail_out;
1609 if( $hold_transit or
1610 $U->copy_status($self->copy->status)->id
1611 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1614 if( $hold_transit ) {
1615 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1617 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1622 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1624 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1625 $self->reshelve_copy(1);
1626 $self->cancelled_hold_transit(1);
1627 $self->notify_hold(0); # don't notify for cancelled holds
1628 return if $self->bail_out;
1632 # hold transited to correct location
1633 $self->checkin_flesh_events;
1638 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1640 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1641 " that is in-transit, but there is no transit.. repairing");
1642 $self->reshelve_copy(1);
1643 return if $self->bail_out;
1646 if( $self->is_renewal ) {
1647 $self->push_events(OpenILS::Event->new('SUCCESS'));
1651 # ------------------------------------------------------------------------------
1652 # Circulations and transits are now closed where necessary. Now go on to see if
1653 # this copy can fulfill a hold or needs to be routed to a different location
1654 # ------------------------------------------------------------------------------
1656 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1657 return if $self->bail_out;
1659 } else { # not needed for a hold
1661 my $circ_lib = (ref $self->copy->circ_lib) ?
1662 $self->copy->circ_lib->id : $self->copy->circ_lib;
1664 if( $self->remote_hold ) {
1665 $circ_lib = $self->remote_hold->pickup_lib;
1666 $logger->warn("circulator: Copy ".$self->copy->barcode.
1667 " is on a remote hold's shelf, sending to $circ_lib");
1670 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1672 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1674 $self->checkin_handle_precat();
1675 return if $self->bail_out;
1679 my $bc = $self->copy->barcode;
1680 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1681 $self->checkin_build_copy_transit($circ_lib);
1682 return if $self->bail_out;
1683 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1687 $self->reshelve_copy;
1688 return if $self->bail_out;
1690 unless($self->checkin_changed) {
1692 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1693 my $stat = $U->copy_status($self->copy->status)->id;
1695 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1696 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1697 $self->bail_out(1); # no need to commit anything
1701 $self->push_events(OpenILS::Event->new('SUCCESS'))
1702 unless @{$self->events};
1706 # ------------------------------------------------------------------------------
1707 # Update the patron penalty info in the DB
1708 # ------------------------------------------------------------------------------
1709 $U->update_patron_penalties(
1710 authtoken => $self->editor->authtoken,
1711 patron => $self->patron,
1712 background => 1 ) if $self->is_checkin;
1714 $self->checkin_flesh_events;
1720 my $force = $self->force || shift;
1721 my $copy = $self->copy;
1723 my $stat = $U->copy_status($copy->status)->id;
1726 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1727 $stat != OILS_COPY_STATUS_CATALOGING and
1728 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1729 $stat != OILS_COPY_STATUS_RESHELVING )) {
1731 $copy->status( OILS_COPY_STATUS_RESHELVING );
1733 $self->checkin_changed(1);
1738 # Returns true if the item is at the current location
1739 # because it was transited there for a hold and the
1740 # hold has not been fulfilled
1741 sub checkin_check_holds_shelf {
1743 return 0 unless $self->copy;
1746 $U->copy_status($self->copy->status)->id ==
1747 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1749 # find the hold that put us on the holds shelf
1750 my $holds = $self->editor->search_action_hold_request(
1752 current_copy => $self->copy->id,
1753 capture_time => { '!=' => undef },
1754 fulfillment_time => undef,
1755 cancel_time => undef,
1760 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1761 $self->reshelve_copy(1);
1765 my $hold = $$holds[0];
1767 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1768 $hold->id. "] for copy ".$self->copy->barcode);
1770 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1771 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1775 $logger->info("circulator: hold is not for here..");
1776 $self->remote_hold($hold);
1781 sub checkin_handle_precat {
1783 my $copy = $self->copy;
1785 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1786 $copy->status(OILS_COPY_STATUS_CATALOGING);
1787 $self->update_copy();
1788 $self->checkin_changed(1);
1789 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1794 sub checkin_build_copy_transit {
1797 my $copy = $self->copy;
1798 my $transit = Fieldmapper::action::transit_copy->new;
1800 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1801 $logger->info("circulator: transiting copy to $dest");
1803 $transit->source($self->editor->requestor->ws_ou);
1804 $transit->dest($dest);
1805 $transit->target_copy($copy->id);
1806 $transit->source_send_time('now');
1807 $transit->copy_status( $U->copy_status($copy->status)->id );
1809 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1811 return $self->bail_on_events($self->editor->event)
1812 unless $self->editor->create_action_transit_copy($transit);
1814 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1816 $self->checkin_changed(1);
1820 sub attempt_checkin_hold_capture {
1822 my $copy = $self->copy;
1824 # See if this copy can fulfill any holds
1825 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1826 $self->editor, $copy, $self->editor->requestor );
1829 $logger->debug("circulator: no potential permitted".
1830 "holds found for copy ".$copy->barcode);
1834 $self->retarget($retarget);
1836 $logger->info("circulator: found permitted hold ".
1837 $hold->id . " for copy, capturing...");
1839 $hold->current_copy($copy->id);
1840 $hold->capture_time('now');
1842 # prevent DB errors caused by fetching
1843 # holds from storage, and updating through cstore
1844 $hold->clear_fulfillment_time;
1845 $hold->clear_fulfillment_staff;
1846 $hold->clear_fulfillment_lib;
1847 $hold->clear_expire_time;
1848 $hold->clear_cancel_time;
1849 $hold->clear_prev_check_time unless $hold->prev_check_time;
1851 $self->bail_on_events($self->editor->event)
1852 unless $self->editor->update_action_hold_request($hold);
1854 $self->checkin_changed(1);
1856 return 1 if $self->bail_out;
1858 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1860 # This hold was captured in the correct location
1861 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1862 $self->push_events(OpenILS::Event->new('SUCCESS'));
1864 #$self->do_hold_notify($hold->id);
1865 $self->notify_hold($hold->id);
1869 # Hold needs to be picked up elsewhere. Build a hold
1870 # transit and route the item.
1871 $self->checkin_build_hold_transit();
1872 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1873 return 1 if $self->bail_out;
1875 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1878 # make sure we save the copy status
1883 sub do_hold_notify {
1884 my( $self, $holdid ) = @_;
1886 $logger->info("circulator: running delayed hold notify process");
1888 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1889 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1891 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1892 hold_id => $holdid, requestor => $self->editor->requestor);
1894 $logger->debug("circulator: built hold notifier");
1896 if(!$notifier->event) {
1898 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1900 my $stat = $notifier->send_email_notify;
1901 if( $stat == '1' ) {
1902 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1906 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1909 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1913 sub retarget_holds {
1914 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1915 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1916 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1917 # no reason to wait for the return value
1921 sub checkin_build_hold_transit {
1924 my $copy = $self->copy;
1925 my $hold = $self->hold;
1926 my $trans = Fieldmapper::action::hold_transit_copy->new;
1928 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1930 $trans->hold($hold->id);
1931 $trans->source($self->editor->requestor->ws_ou);
1932 $trans->dest($hold->pickup_lib);
1933 $trans->source_send_time("now");
1934 $trans->target_copy($copy->id);
1936 # when the copy gets to its destination, it will recover
1937 # this status - put it onto the holds shelf
1938 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1940 return $self->bail_on_events($self->editor->event)
1941 unless $self->editor->create_action_hold_transit_copy($trans);
1946 sub process_received_transit {
1948 my $copy = $self->copy;
1949 my $copyid = $self->copy->id;
1951 my $status_name = $U->copy_status($copy->status)->name;
1952 $logger->debug("circulator: attempting transit receive on ".
1953 "copy $copyid. Copy status is $status_name");
1955 my $transit = $self->transit;
1957 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1958 # - this item is in-transit to a different location
1960 my $tid = $transit->id;
1961 my $loc = $self->editor->requestor->ws_ou;
1962 my $dest = $transit->dest;
1964 $logger->info("circulator: Fowarding transit on copy which is destined ".
1965 "for a different location. transit=$tid, copy=$copyid, current ".
1966 "location=$loc, destination location=$dest");
1968 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1970 # grab the associated hold object if available
1971 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1972 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1974 return $self->bail_on_events($evt);
1977 # The transit is received, set the receive time
1978 $transit->dest_recv_time('now');
1979 $self->bail_on_events($self->editor->event)
1980 unless $self->editor->update_action_transit_copy($transit);
1982 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1984 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1985 $copy->status( $transit->copy_status );
1986 $self->update_copy();
1987 return if $self->bail_out;
1991 #$self->do_hold_notify($hold_transit->hold);
1992 $self->notify_hold($hold_transit->hold);
1997 OpenILS::Event->new(
2000 payload => { transit => $transit, holdtransit => $hold_transit } ));
2002 return $hold_transit;
2006 sub checkin_handle_circ {
2010 my $circ = $self->circ;
2011 my $copy = $self->copy;
2015 # backdate the circ if necessary
2016 if($self->backdate) {
2017 $self->checkin_handle_backdate;
2018 return if $self->bail_out;
2021 if(!$circ->stop_fines) {
2022 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2023 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2024 $circ->stop_fines_time('now') unless $self->backdate;
2025 $circ->stop_fines_time($self->backdate) if $self->backdate;
2028 # see if there are any fines owed on this circ. if not, close it
2029 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2030 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2032 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2034 # Set the checkin vars since we have the item
2035 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2037 $circ->checkin_staff($self->editor->requestor->id);
2038 $circ->checkin_lib($self->editor->requestor->ws_ou);
2040 my $circ_lib = (ref $self->copy->circ_lib) ?
2041 $self->copy->circ_lib->id : $self->copy->circ_lib;
2042 my $stat = $U->copy_status($self->copy->status)->id;
2044 # If the item is lost/missing and it needs to be sent home, don't
2045 # reshelve the copy, leave it lost/missing so the recipient will know
2046 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2047 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2048 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2051 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2055 return $self->bail_on_events($self->editor->event)
2056 unless $self->editor->update_action_circulation($circ);
2060 sub checkin_handle_backdate {
2063 my $bd = $self->backdate;
2065 # ------------------------------------------------------------------
2066 # clean up the backdate for date comparison
2067 # we want any bills created on or after the backdate
2068 # ------------------------------------------------------------------
2069 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2070 #$bd = "${bd}T23:59:59";
2072 my $bills = $self->editor->search_money_billing(
2074 billing_ts => { '>=' => $bd },
2075 xact => $self->circ->id,
2076 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2080 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2082 for my $bill (@$bills) {
2083 unless( $U->is_true($bill->voided) ) {
2084 $logger->info("backdate voiding bill ".$bill->id);
2086 $bill->void_time('now');
2087 $bill->voider($self->editor->requestor->id);
2088 my $n = $bill->note || "";
2089 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2091 $self->bail_on_events($self->editor->event)
2092 unless $self->editor->update_money_billing($bill);
2100 # XXX Legacy version for Circ.pm support
2101 sub _checkin_handle_backdate {
2102 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
2105 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2106 $bd = "${bd}T23:59:59";
2108 my $bills = $session->request(
2109 "open-ils.storage.direct.money.billing.search_where.atomic",
2110 billing_ts => { '>=' => $bd },
2112 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2115 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2118 for my $bill (@$bills) {
2119 unless( $U->is_true($bill->voided) ) {
2120 $logger->debug("voiding bill ".$bill->id);
2122 $bill->void_time('now');
2123 $bill->voider($requestor->id);
2124 my $n = $bill->note || "";
2125 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
2126 my $s = $session->request(
2127 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
2128 return $U->DB_UPDATE_FAILED($bill) unless $s;
2142 sub find_patron_from_copy {
2144 my $circs = $self->editor->search_action_circulation(
2145 { target_copy => $self->copy->id, checkin_time => undef });
2146 my $circ = $circs->[0];
2147 return unless $circ;
2148 my $u = $self->editor->retrieve_actor_user($circ->usr)
2149 or return $self->bail_on_events($self->editor->event);
2153 sub check_checkin_copy_status {
2155 my $copy = $self->copy;
2161 my $status = $U->copy_status($copy->status)->id;
2164 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2165 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2166 $status == OILS_COPY_STATUS_IN_PROCESS ||
2167 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2168 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2169 $status == OILS_COPY_STATUS_CATALOGING ||
2170 $status == OILS_COPY_STATUS_RESHELVING );
2172 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2173 if( $status == OILS_COPY_STATUS_LOST );
2175 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2176 if( $status == OILS_COPY_STATUS_MISSING );
2178 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2183 # --------------------------------------------------------------------------
2184 # On checkin, we need to return as many relevant objects as we can
2185 # --------------------------------------------------------------------------
2186 sub checkin_flesh_events {
2189 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2190 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2191 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2195 for my $evt (@{$self->events}) {
2198 $payload->{copy} = $U->unflesh_copy($self->copy);
2199 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2200 $payload->{circ} = $self->circ;
2201 $payload->{transit} = $self->transit;
2202 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2204 # $self->hold may or may not have been replaced with a
2205 # valid hold after processing a cancelled hold
2206 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2208 $evt->{payload} = $payload;
2213 my( $self, $msg ) = @_;
2214 my $bc = ($self->copy) ? $self->copy->barcode :
2217 my $usr = ($self->patron) ? $self->patron->id : "";
2218 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2219 ", recipient=$usr, copy=$bc");
2225 $self->log_me("do_renew()");
2226 $self->is_renewal(1);
2228 # Make sure there is an open circ to renew that is not
2229 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2230 my $circ = $self->editor->search_action_circulation(
2231 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2234 $circ = $self->editor->search_action_circulation(
2236 target_copy => $self->copy->id,
2237 stop_fines => OILS_STOP_FINES_MAX_FINES,
2238 checkin_time => undef
2243 return $self->bail_on_events($self->editor->event) unless $circ;
2245 # A user is not allowed to renew another user's items without permission
2246 unless( $circ->usr eq $self->editor->requestor->id ) {
2247 return $self->bail_on_events($self->editor->events)
2248 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2251 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2252 if $circ->renewal_remaining < 1;
2254 # -----------------------------------------------------------------
2256 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2259 $self->run_renew_permit;
2262 $self->do_checkin();
2263 return if $self->bail_out;
2265 unless( $self->permit_override ) {
2267 return if $self->bail_out;
2268 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2269 $self->remove_event('ITEM_NOT_CATALOGED');
2272 $self->override_events;
2273 return if $self->bail_out;
2276 $self->do_checkout();
2281 my( $self, $evt ) = @_;
2282 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2283 $logger->debug("circulator: removing event from list: $evt");
2284 my @events = @{$self->events};
2285 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2290 my( $self, $evt ) = @_;
2291 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2292 return grep { $_->{textcode} eq $evt } @{$self->events};
2297 sub run_renew_permit {
2302 if(!$self->legacy_script_support) {
2303 my $results = $self->run_indb_circ_test;
2304 unless($self->circ_test_success) {
2305 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2310 my $runner = $self->script_runner;
2312 $runner->load($self->circ_permit_renew);
2313 my $result = $runner->run or
2314 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2315 my $events = $result->{events};
2318 $logger->activity("ciculator: circ_permit_renew for user ".
2319 $self->patron->id." returned events: @$events") if @$events;
2321 $self->push_events(OpenILS::Event->new($_)) for @$events;
2323 $logger->debug("circulator: re-creating script runner to be safe");
2324 #$self->mk_script_runner;