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{request_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
367 recurring_fines_level
380 cancelled_hold_transit
389 legacy_script_support
401 my $type = ref($self) or die "$self is not an object";
403 my $name = $AUTOLOAD;
406 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
407 $logger->error("circulator: $type: invalid autoload field: $name");
408 die "$type: invalid autoload field: $name\n"
413 *{"${type}::${name}"} = sub {
416 $s->{$name} = $v if defined $v;
420 return $self->$name($data);
425 my( $class, $auth, %args ) = @_;
426 $class = ref($class) || $class;
427 my $self = bless( {}, $class );
431 new_editor(xact => 1, authtoken => $auth) );
433 unless( $self->editor->checkauth ) {
434 $self->bail_on_events($self->editor->event);
438 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
440 $self->$_($args{$_}) for keys %args;
443 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
445 # if this is a renewal, default to desk_renewal
446 $self->desk_renewal(1) unless
447 $self->opac_renewal or $self->phone_renewal;
449 $self->capture('') unless $self->capture;
455 # --------------------------------------------------------------------------
456 # True if we should discontinue processing
457 # --------------------------------------------------------------------------
459 my( $self, $bool ) = @_;
460 if( defined $bool ) {
461 $logger->info("circulator: BAILING OUT") if $bool;
462 $self->{bail_out} = $bool;
464 return $self->{bail_out};
469 my( $self, @evts ) = @_;
472 $logger->info("circulator: pushing event ".$e->{textcode});
473 push( @{$self->events}, $e ) unless
474 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
480 my $key = md5_hex( time() . rand() . "$$" );
481 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
482 return $self->permit_key($key);
485 sub check_permit_key {
487 my $key = $self->permit_key;
488 return 0 unless $key;
489 my $k = "oils_permit_key_$key";
490 my $one = $self->cache_handle->get_cache($k);
491 $self->cache_handle->delete_cache($k);
492 return ($one) ? 1 : 0;
497 my $e = $self->editor;
499 # --------------------------------------------------------------------------
500 # Grab the fleshed copy
501 # --------------------------------------------------------------------------
502 unless($self->is_noncat) {
506 flesh_fields => {acp => ['call_number'], acn => ['record']}
509 $copy = $e->retrieve_asset_copy(
510 [$self->copy_id, $flesh ]) or return $e->event;
512 } elsif( $self->copy_barcode ) {
514 $copy = $e->search_asset_copy(
515 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
520 $self->volume($copy->call_number);
521 $self->title($self->volume->record);
522 $self->copy->call_number($self->volume->id);
523 $self->volume->record($self->title->id);
524 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
525 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
526 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
527 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
530 # We can't renew if there is no copy
531 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
532 if $self->is_renewal;
537 return undef if $self->is_checkin;
539 # --------------------------------------------------------------------------
541 # --------------------------------------------------------------------------
543 if( $self->patron_id ) {
544 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
546 } elsif( $self->patron_barcode ) {
548 my $card = $e->search_actor_card(
549 {barcode => $self->patron_barcode})->[0] or return $e->event;
551 $patron = $e->search_actor_user(
552 {card => $card->id})->[0] or return $e->event;
555 if( my $copy = $self->copy ) {
556 my $circs = $e->search_action_circulation(
557 {target_copy => $copy->id, checkin_time => undef});
559 if( my $circ = $circs->[0] ) {
560 $patron = $e->retrieve_actor_user($circ->usr)
566 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
567 unless $self->patron($patron);
570 # --------------------------------------------------------------------------
571 # This builds the script runner environment and fetches most of the
573 # --------------------------------------------------------------------------
574 sub mk_script_runner {
580 qw/copy copy_barcode copy_id patron
581 patron_id patron_barcode volume title editor/;
583 # Translate our objects into the ScriptBuilder args hash
584 $$args{$_} = $self->$_() for @fields;
586 $args->{ignore_user_status} = 1 if $self->is_checkin;
587 $$args{fetch_patron_by_circ_copy} = 1;
588 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
590 if( my $pco = $self->pending_checkouts ) {
591 $logger->info("circulator: we were given a pending checkouts number of $pco");
592 $$args{patronItemsOut} = $pco;
595 # This fetches most of the objects we need
596 $self->script_runner(
597 OpenILS::Application::Circ::ScriptBuilder->build($args));
599 # Now we translate the ScriptBuilder objects back into self
600 $self->$_($$args{$_}) for @fields;
602 my @evts = @{$args->{_events}} if $args->{_events};
604 $logger->debug("circulator: script builder returned events: @evts") if @evts;
608 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
609 if(!$self->is_noncat and
611 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
615 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
616 return $self->bail_on_events(@e);
621 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
622 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
623 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
624 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
628 # We can't renew if there is no copy
629 return $self->bail_on_events(@evts) if
630 $self->is_renewal and !$self->copy;
632 # Set some circ-specific flags in the script environment
633 my $evt = "environment";
634 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
636 if( $self->is_noncat ) {
637 $self->script_runner->insert("$evt.isNonCat", 1);
638 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
641 if( $self->is_precat ) {
642 $self->script_runner->insert("environment.isPrecat", 1, 1);
645 $self->script_runner->add_path( $_ ) for @$script_libs;
650 # --------------------------------------------------------------------------
651 # Does the circ permit work
652 # --------------------------------------------------------------------------
656 $self->log_me("do_permit()");
658 unless( $self->editor->requestor->id == $self->patron->id ) {
659 return $self->bail_on_events($self->editor->event)
660 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
663 $self->check_captured_holds();
664 $self->do_copy_checks();
665 return if $self->bail_out;
666 $self->run_patron_permit_scripts();
667 $self->run_copy_permit_scripts()
668 unless $self->is_precat or $self->is_noncat;
669 $self->check_item_deposit_events();
670 $self->override_events() unless
671 $self->is_renewal and not $self->check_penalty_on_renew;
672 return if $self->bail_out;
674 if($self->is_precat and not $self->request_precat) {
677 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
678 return $self->bail_out(1) unless $self->is_renewal;
684 payload => $self->mk_permit_key));
687 sub check_item_deposit_events {
689 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED')) if $self->is_deposit;
690 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED')) if $self->is_rental;
693 sub check_captured_holds {
695 my $copy = $self->copy;
696 my $patron = $self->patron;
698 return undef unless $copy;
700 my $s = $U->copy_status($copy->status)->id;
701 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
702 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
704 # Item is on the holds shelf, make sure it's going to the right person
705 my $holds = $self->editor->search_action_hold_request(
708 current_copy => $copy->id ,
709 capture_time => { '!=' => undef },
710 cancel_time => undef,
711 fulfillment_time => undef
717 if( $holds and $$holds[0] ) {
718 return undef if $$holds[0]->usr == $patron->id;
721 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
723 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
729 my $copy = $self->copy;
732 my $stat = $U->copy_status($copy->status)->id;
734 # We cannot check out a copy if it is in-transit
735 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
736 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
739 $self->handle_claims_returned();
740 return if $self->bail_out;
742 # no claims returned circ was found, check if there is any open circ
743 unless( $self->is_renewal ) {
744 my $circs = $self->editor->search_action_circulation(
745 { target_copy => $copy->id, checkin_time => undef }
748 return $self->bail_on_events(
749 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
754 sub send_penalty_request {
756 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
757 $self->penalty_request(
759 'open-ils.penalty.patron_penalty.calculate',
761 authtoken => $self->editor->authtoken,
762 patron => $self->patron } ) );
765 sub gather_penalty_request {
767 return [] unless $self->penalty_request;
768 my $data = $self->penalty_request->recv;
770 throw $data if UNIVERSAL::isa($data,'Error');
771 $data = $data->content;
772 return $data->{fatal_penalties};
774 $logger->error("circulator: penalty request returned no data");
778 my $LEGACY_CIRC_EVENT_MAP = {
779 'actor.usr.barred' => 'PATRON_BARRED',
780 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
781 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
782 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
783 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
784 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
785 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
786 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
787 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
791 # ---------------------------------------------------------------------
792 # This pushes any patron-related events into the list but does not
793 # set bail_out for any events
794 # ---------------------------------------------------------------------
795 sub run_patron_permit_scripts {
797 my $runner = $self->script_runner;
798 my $patronid = $self->patron->id;
802 if(!$self->legacy_script_support) {
804 my $results = $self->run_indb_circ_test;
805 unless($self->circ_test_success) {
806 push(@allevents, OpenILS::Event->new(
807 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
812 $self->send_penalty_request() unless
813 $self->is_renewal and not $self->check_penalty_on_renew;
816 # --------------------------------------------------------------------- # Now run the patron permit script
817 # ---------------------------------------------------------------------
818 $runner->load($self->circ_permit_patron);
819 my $result = $runner->run or
820 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
822 my $patron_events = $result->{events};
824 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
825 [] : $self->gather_penalty_request();
827 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
830 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
832 $self->push_events(@allevents);
835 sub run_indb_circ_test {
837 return $self->matrix_test_result if $self->matrix_test_result;
839 my $dbfunc = ($self->is_renewal) ?
840 'action.item_user_renew_test' : 'action.item_user_circ_test';
842 my $results = ($self->matrix_test_result) ?
843 $self->matrix_test_result :
844 $self->editor->json_query(
847 $self->editor->requestor->ws_ou,
848 ($self->is_precat) ? undef : $self->copy->id,
854 $self->circ_test_success($U->is_true($results->[0]->{success}));
855 if($self->circ_test_success) {
856 $self->circ_matrix_test(
857 $self->editor->retrieve_config_circ_matrix_test(
858 $results->[0]->{matchpoint}
863 if($self->circ_test_success) {
864 $self->circ_matrix_ruleset(
865 $self->editor->retrieve_config_circ_matrix_ruleset([
866 $results->[0]->{matchpoint},
869 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
877 return $self->matrix_test_result($results);
882 $self->run_indb_circ_test;
885 circ_test_success => $self->circ_test_success,
886 failure_events => [],
890 unless($self->circ_test_success) {
891 push(@{$results->{failure_codes}},
892 $_->{fail_part}) for @{$self->matrix_test_result};
893 push(@{$results->{failure_events}},
894 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
895 for @{$self->matrix_test_result};
899 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
900 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
901 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
903 my $policy = $self->get_circ_policy(
904 $duration_rule, $recurring_fine_rule, $max_fine_rule);
906 $$results{$_} = $$policy{$_} for keys %$policy;
910 # ---------------------------------------------------------------------
911 # Loads the circ policy info for duration, recurring fine, and max
912 # fine based on the current copy
913 # ---------------------------------------------------------------------
914 sub get_circ_policy {
915 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
918 duration_rule => $duration_rule->name,
919 recurring_fine_rule => $recurring_fine_rule->name,
920 max_fine_rule => $max_fine_rule->name,
921 max_fine => $self->get_max_fine_amount($max_fine_rule),
922 fine_interval => $recurring_fine_rule->recurance_interval,
923 renewal_remaining => $duration_rule->max_renewals
926 $policy->{duration} = $duration_rule->shrt
927 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
928 $policy->{duration} = $duration_rule->normal
929 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
930 $policy->{duration} = $duration_rule->extended
931 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
933 $policy->{recurring_fine} = $recurring_fine_rule->low
934 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
935 $policy->{recurring_fine} = $recurring_fine_rule->normal
936 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
937 $policy->{recurring_fine} = $recurring_fine_rule->high
938 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
943 sub get_max_fine_amount {
945 my $max_fine_rule = shift;
946 my $max_amount = $max_fine_rule->amount;
948 # if is_percent is true then the max->amount is
949 # use as a percentage of the copy price
950 if ($U->is_true($max_fine_rule->is_percent)) {
951 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
952 $max_amount = $price * $max_fine_rule->amount / 100;
960 sub run_copy_permit_scripts {
962 my $copy = $self->copy || return;
963 my $runner = $self->script_runner;
967 if(!$self->legacy_script_support) {
968 my $results = $self->run_indb_circ_test;
969 unless($self->circ_test_success) {
970 push(@allevents, OpenILS::Event->new(
971 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
975 # ---------------------------------------------------------------------
976 # Capture all of the copy permit events
977 # ---------------------------------------------------------------------
978 $runner->load($self->circ_permit_copy);
979 my $result = $runner->run or
980 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
981 my $copy_events = $result->{events};
983 # ---------------------------------------------------------------------
984 # Now collect all of the events together
985 # ---------------------------------------------------------------------
986 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
989 # See if this copy has an alert message
990 my $ae = $self->check_copy_alert();
991 push( @allevents, $ae ) if $ae;
993 # uniquify the events
994 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
995 @allevents = values %hash;
998 $_->{payload} = $copy if
999 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1002 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1004 $self->push_events(@allevents);
1008 sub check_copy_alert {
1010 return undef if $self->is_renewal;
1011 return OpenILS::Event->new(
1012 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1013 if $self->copy and $self->copy->alert_message;
1019 # --------------------------------------------------------------------------
1020 # If the call is overriding and has permissions to override every collected
1021 # event, the are cleared. Any event that the caller does not have
1022 # permission to override, will be left in the event list and bail_out will
1024 # XXX We need code in here to cancel any holds/transits on copies
1025 # that are being force-checked out
1026 # --------------------------------------------------------------------------
1027 sub override_events {
1029 my @events = @{$self->events};
1030 return unless @events;
1032 if(!$self->override) {
1033 return $self->bail_out(1)
1034 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1039 for my $e (@events) {
1040 my $tc = $e->{textcode};
1041 next if $tc eq 'SUCCESS';
1042 my $ov = "$tc.override";
1043 $logger->info("circulator: attempting to override event: $ov");
1045 return $self->bail_on_events($self->editor->event)
1046 unless( $self->editor->allowed($ov) );
1051 # --------------------------------------------------------------------------
1052 # If there is an open claimsreturn circ on the requested copy, close the
1053 # circ if overriding, otherwise bail out
1054 # --------------------------------------------------------------------------
1055 sub handle_claims_returned {
1057 my $copy = $self->copy;
1059 my $CR = $self->editor->search_action_circulation(
1061 target_copy => $copy->id,
1062 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1063 checkin_time => undef,
1067 return unless ($CR = $CR->[0]);
1071 # - If the caller has set the override flag, we will check the item in
1072 if($self->override) {
1074 $CR->checkin_time('now');
1075 $CR->checkin_lib($self->editor->requestor->ws_ou);
1076 $CR->checkin_staff($self->editor->requestor->id);
1078 $evt = $self->editor->event
1079 unless $self->editor->update_action_circulation($CR);
1082 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1085 $self->bail_on_events($evt) if $evt;
1090 # --------------------------------------------------------------------------
1091 # This performs the checkout
1092 # --------------------------------------------------------------------------
1096 $self->log_me("do_checkout()");
1098 # make sure perms are good if this isn't a renewal
1099 unless( $self->is_renewal ) {
1100 return $self->bail_on_events($self->editor->event)
1101 unless( $self->editor->allowed('COPY_CHECKOUT') );
1104 # verify the permit key
1105 unless( $self->check_permit_key ) {
1106 if( $self->permit_override ) {
1107 return $self->bail_on_events($self->editor->event)
1108 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1110 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1114 # if this is a non-cataloged circ, build the circ and finish
1115 if( $self->is_noncat ) {
1116 $self->checkout_noncat;
1118 OpenILS::Event->new('SUCCESS',
1119 payload => { noncat_circ => $self->circ }));
1123 if( $self->is_precat ) {
1124 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1125 $self->make_precat_copy;
1126 return if $self->bail_out;
1128 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1129 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1132 $self->do_copy_checks;
1133 return if $self->bail_out;
1135 $self->run_checkout_scripts();
1136 return if $self->bail_out;
1138 $self->build_checkout_circ_object();
1139 return if $self->bail_out;
1141 $self->apply_modified_due_date();
1142 return if $self->bail_out;
1144 return $self->bail_on_events($self->editor->event)
1145 unless $self->editor->create_action_circulation($self->circ);
1147 # refresh the circ to force local time zone for now
1148 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1150 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1152 return if $self->bail_out;
1154 $self->apply_deposit_fee();
1155 return if $self->bail_out;
1157 $self->handle_checkout_holds();
1158 return if $self->bail_out;
1160 # ------------------------------------------------------------------------------
1161 # Update the patron penalty info in the DB. Run it for permit-overrides or
1162 # renewals since both of those cases do not require the penalty server to
1163 # run during the permit phase of the checkout
1164 # ------------------------------------------------------------------------------
1165 if( $self->permit_override or $self->is_renewal ) {
1166 $U->update_patron_penalties(
1167 authtoken => $self->editor->authtoken,
1168 patron => $self->patron,
1173 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1175 OpenILS::Event->new('SUCCESS',
1177 copy => $U->unflesh_copy($self->copy),
1178 circ => $self->circ,
1180 holds_fulfilled => $self->fulfilled_holds,
1181 deposit_billing => $self->deposit_billing,
1182 rental_billing => $self->rental_billing
1188 sub apply_deposit_fee {
1190 my $copy = $self->copy;
1191 return unless $self->is_deposit or $self->is_rental;
1193 my $bill = Fieldmapper::money::billing->new;
1194 my $amount = $copy->deposit_amount;
1197 if($self->is_deposit) {
1198 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1199 $self->deposit_billing($bill);
1201 $billing_type = OILS_BILLING_TYPE_RENTAL;
1202 $self->rental_billing($bill);
1205 $bill->xact($self->circ->id);
1206 $bill->amount($amount);
1207 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1208 $bill->billing_type($billing_type);
1209 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1211 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1216 my $copy = $self->copy;
1218 my $stat = $copy->status if ref $copy->status;
1219 my $loc = $copy->location if ref $copy->location;
1220 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1222 $copy->status($stat->id) if $stat;
1223 $copy->location($loc->id) if $loc;
1224 $copy->circ_lib($circ_lib->id) if $circ_lib;
1225 $copy->editor($self->editor->requestor->id);
1226 $copy->edit_date('now');
1227 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1229 return $self->bail_on_events($self->editor->event)
1230 unless $self->editor->update_asset_copy($self->copy);
1232 $copy->status($U->copy_status($copy->status));
1233 $copy->location($loc) if $loc;
1234 $copy->circ_lib($circ_lib) if $circ_lib;
1238 sub bail_on_events {
1239 my( $self, @evts ) = @_;
1240 $self->push_events(@evts);
1244 sub handle_checkout_holds {
1247 my $copy = $self->copy;
1248 my $patron = $self->patron;
1250 my $holds = $self->editor->search_action_hold_request(
1252 current_copy => $copy->id ,
1253 cancel_time => undef,
1254 fulfillment_time => undef
1260 # XXX We should only fulfill one hold here...
1261 # XXX If a hold was transited to the user who is checking out
1262 # the item, we need to make sure that hold is what's grabbed
1265 # for now, just sort by id to get what should be the oldest hold
1266 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1267 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1268 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1271 my $hold = $myholds[0];
1273 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1275 # if the hold was never officially captured, capture it.
1276 $hold->capture_time('now') unless $hold->capture_time;
1278 # just make sure it's set correctly
1279 $hold->current_copy($copy->id);
1281 $hold->fulfillment_time('now');
1282 $hold->fulfillment_staff($self->editor->requestor->id);
1283 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1285 return $self->bail_on_events($self->editor->event)
1286 unless $self->editor->update_action_hold_request($hold);
1288 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1290 push( @fulfilled, $hold->id );
1293 # If there are any holds placed for other users that point to this copy,
1294 # then we need to un-target those holds so the targeter can pick a new copy
1297 $logger->info("circulator: un-targeting hold ".$_->id.
1298 " because copy ".$copy->id." is getting checked out");
1300 # - make the targeter process this hold at next run
1301 $_->clear_prev_check_time;
1303 # - clear out the targetted copy
1304 $_->clear_current_copy;
1305 $_->clear_capture_time;
1307 return $self->bail_on_event($self->editor->event)
1308 unless $self->editor->update_action_hold_request($_);
1312 $self->fulfilled_holds(\@fulfilled);
1317 sub run_checkout_scripts {
1321 my $runner = $self->script_runner;
1330 if(!$self->legacy_script_support) {
1331 $self->run_indb_circ_test();
1332 $duration = $self->circ_matrix_ruleset->duration_rule;
1333 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1334 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1338 $runner->load($self->circ_duration);
1340 my $result = $runner->run or
1341 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1343 $duration_name = $result->{durationRule};
1344 $recurring_name = $result->{recurringFinesRule};
1345 $max_fine_name = $result->{maxFine};
1348 $duration_name = $duration->name if $duration;
1349 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1352 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1353 return $self->bail_on_events($evt) if $evt;
1355 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1356 return $self->bail_on_events($evt) if $evt;
1358 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1359 return $self->bail_on_events($evt) if $evt;
1364 # The item circulates with an unlimited duration
1370 $self->duration_rule($duration);
1371 $self->recurring_fines_rule($recurring);
1372 $self->max_fine_rule($max_fine);
1376 sub build_checkout_circ_object {
1379 my $circ = Fieldmapper::action::circulation->new;
1380 my $duration = $self->duration_rule;
1381 my $max = $self->max_fine_rule;
1382 my $recurring = $self->recurring_fines_rule;
1383 my $copy = $self->copy;
1384 my $patron = $self->patron;
1388 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1390 my $dname = $duration->name;
1391 my $mname = $max->name;
1392 my $rname = $recurring->name;
1394 $logger->debug("circulator: building circulation ".
1395 "with duration=$dname, maxfine=$mname, recurring=$rname");
1397 $circ->duration($policy->{duration});
1398 $circ->recuring_fine($policy->{recurring_fine});
1399 $circ->duration_rule($duration->name);
1400 $circ->recuring_fine_rule($recurring->name);
1401 $circ->max_fine_rule($max->name);
1402 $circ->max_fine($policy->{max_fine});
1403 $circ->fine_interval($recurring->recurance_interval);
1404 $circ->renewal_remaining($duration->max_renewals);
1408 $logger->info("circulator: copy found with an unlimited circ duration");
1409 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1410 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1411 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1412 $circ->renewal_remaining(0);
1415 $circ->target_copy( $copy->id );
1416 $circ->usr( $patron->id );
1417 $circ->circ_lib( $self->circ_lib );
1419 if( $self->is_renewal ) {
1420 $circ->opac_renewal('t') if $self->opac_renewal;
1421 $circ->phone_renewal('t') if $self->phone_renewal;
1422 $circ->desk_renewal('t') if $self->desk_renewal;
1423 $circ->renewal_remaining($self->renewal_remaining);
1424 $circ->circ_staff($self->editor->requestor->id);
1428 # if the user provided an overiding checkout time,
1429 # (e.g. the checkout really happened several hours ago), then
1430 # we apply that here. Does this need a perm??
1431 $circ->xact_start(clense_ISO8601($self->checkout_time))
1432 if $self->checkout_time;
1434 # if a patron is renewing, 'requestor' will be the patron
1435 $circ->circ_staff($self->editor->requestor->id);
1436 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1442 sub apply_modified_due_date {
1444 my $circ = $self->circ;
1445 my $copy = $self->copy;
1447 if( $self->due_date ) {
1449 return $self->bail_on_events($self->editor->event)
1450 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1452 $circ->due_date(clense_ISO8601($self->due_date));
1456 # if the due_date lands on a day when the location is closed
1457 return unless $copy and $circ->due_date;
1459 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1461 # due-date overlap should be determined by the location the item
1462 # is checked out from, not the owning or circ lib of the item
1463 my $org = $self->editor->requestor->ws_ou;
1465 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1466 " with an item due date of ".$circ->due_date );
1468 my $dateinfo = $U->storagereq(
1469 'open-ils.storage.actor.org_unit.closed_date.overlap',
1470 $org, $circ->due_date );
1473 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1474 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1476 # XXX make the behavior more dynamic
1477 # for now, we just push the due date to after the close date
1478 $circ->due_date($dateinfo->{end});
1485 sub create_due_date {
1486 my( $self, $duration ) = @_;
1487 # if there is a raw time component (e.g. from postgres),
1488 # turn it into an interval that interval_to_seconds can parse
1489 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1490 my ($sec,$min,$hour,$mday,$mon,$year) =
1491 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1492 $year += 1900; $mon += 1;
1493 my $due_date = sprintf(
1494 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1495 $year, $mon, $mday, $hour, $min, $sec);
1501 sub make_precat_copy {
1503 my $copy = $self->copy;
1506 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1508 $copy->editor($self->editor->requestor->id);
1509 $copy->edit_date('now');
1510 $copy->dummy_title($self->dummy_title);
1511 $copy->dummy_author($self->dummy_author);
1513 $self->update_copy();
1517 $logger->info("circulator: Creating a new precataloged ".
1518 "copy in checkout with barcode " . $self->copy_barcode);
1520 $copy = Fieldmapper::asset::copy->new;
1521 $copy->circ_lib($self->circ_lib);
1522 $copy->creator($self->editor->requestor->id);
1523 $copy->editor($self->editor->requestor->id);
1524 $copy->barcode($self->copy_barcode);
1525 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1526 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1527 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1529 $copy->dummy_title($self->dummy_title || "");
1530 $copy->dummy_author($self->dummy_author || "");
1532 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1534 $self->push_events($self->editor->event);
1538 # this is a little bit of a hack, but we need to
1539 # get the copy into the script runner
1540 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1544 sub checkout_noncat {
1550 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1551 my $count = $self->noncat_count || 1;
1552 my $cotime = clense_ISO8601($self->checkout_time) || "";
1554 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1558 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1559 $self->editor->requestor->id,
1567 $self->push_events($evt);
1578 $self->log_me("do_checkin()");
1581 return $self->bail_on_events(
1582 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1585 if( $self->checkin_check_holds_shelf() ) {
1586 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1587 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1588 $self->checkin_flesh_events;
1592 unless( $self->is_renewal ) {
1593 return $self->bail_on_events($self->editor->event)
1594 unless $self->editor->allowed('COPY_CHECKIN');
1597 $self->push_events($self->check_copy_alert());
1598 $self->push_events($self->check_checkin_copy_status());
1600 # the renew code will have already found our circulation object
1601 unless( $self->is_renewal and $self->circ ) {
1602 my $circs = $self->editor->search_action_circulation(
1603 { target_copy => $self->copy->id, checkin_time => undef });
1604 $self->circ($$circs[0]);
1606 # for now, just warn if there are multiple open circs on a copy
1607 $logger->warn("circulator: we have ".scalar(@$circs).
1608 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1611 # if the circ is marked as 'claims returned', add the event to the list
1612 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1613 if ($self->circ and $self->circ->stop_fines
1614 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1616 $self->check_circ_deposit();
1618 # handle the overridable events
1619 $self->override_events unless $self->is_renewal;
1620 return if $self->bail_out;
1624 $self->editor->search_action_transit_copy(
1625 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1629 $self->checkin_handle_circ;
1630 return if $self->bail_out;
1631 $self->checkin_changed(1);
1633 } elsif( $self->transit ) {
1634 my $hold_transit = $self->process_received_transit;
1635 $self->checkin_changed(1);
1637 if( $self->bail_out ) {
1638 $self->checkin_flesh_events;
1642 if( my $e = $self->check_checkin_copy_status() ) {
1643 # If the original copy status is special, alert the caller
1644 my $ev = $self->events;
1645 $self->events([$e]);
1646 $self->override_events;
1647 return if $self->bail_out;
1651 if( $hold_transit or
1652 $U->copy_status($self->copy->status)->id
1653 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1656 if( $hold_transit ) {
1657 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1659 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1664 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1666 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1667 $self->reshelve_copy(1);
1668 $self->cancelled_hold_transit(1);
1669 $self->notify_hold(0); # don't notify for cancelled holds
1670 return if $self->bail_out;
1674 # hold transited to correct location
1675 $self->checkin_flesh_events;
1680 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1682 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1683 " that is in-transit, but there is no transit.. repairing");
1684 $self->reshelve_copy(1);
1685 return if $self->bail_out;
1688 if( $self->is_renewal ) {
1689 $self->push_events(OpenILS::Event->new('SUCCESS'));
1693 # ------------------------------------------------------------------------------
1694 # Circulations and transits are now closed where necessary. Now go on to see if
1695 # this copy can fulfill a hold or needs to be routed to a different location
1696 # ------------------------------------------------------------------------------
1698 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1700 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1701 return if $self->bail_out;
1703 unless($needed_for_hold) {
1704 my $circ_lib = (ref $self->copy->circ_lib) ?
1705 $self->copy->circ_lib->id : $self->copy->circ_lib;
1707 if( $self->remote_hold ) {
1708 $circ_lib = $self->remote_hold->pickup_lib;
1709 $logger->warn("circulator: Copy ".$self->copy->barcode.
1710 " is on a remote hold's shelf, sending to $circ_lib");
1713 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1715 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1717 $self->checkin_handle_precat();
1718 return if $self->bail_out;
1722 my $bc = $self->copy->barcode;
1723 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1724 $self->checkin_build_copy_transit($circ_lib);
1725 return if $self->bail_out;
1726 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1731 $self->reshelve_copy;
1732 return if $self->bail_out;
1734 unless($self->checkin_changed) {
1736 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1737 my $stat = $U->copy_status($self->copy->status)->id;
1739 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1740 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1741 $self->bail_out(1); # no need to commit anything
1745 $self->push_events(OpenILS::Event->new('SUCCESS'))
1746 unless @{$self->events};
1750 # ------------------------------------------------------------------------------
1751 # Update the patron penalty info in the DB
1752 # ------------------------------------------------------------------------------
1753 $U->update_patron_penalties(
1754 authtoken => $self->editor->authtoken,
1755 patron => $self->patron,
1756 background => 1 ) if $self->is_checkin;
1758 $self->checkin_flesh_events;
1762 # if a deposit was payed for this item, push the event
1763 sub check_circ_deposit {
1765 return unless $self->circ;
1766 my $deposit = $self->editor->search_money_billing(
1767 { billing_type => OILS_BILLING_TYPE_DEPOSIT,
1768 xact => $self->circ->id,
1770 }, {idlist => 1})->[0];
1772 $self->push_events(OpenILS::Event->new(
1773 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1778 my $force = $self->force || shift;
1779 my $copy = $self->copy;
1781 my $stat = $U->copy_status($copy->status)->id;
1784 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1785 $stat != OILS_COPY_STATUS_CATALOGING and
1786 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1787 $stat != OILS_COPY_STATUS_RESHELVING )) {
1789 $copy->status( OILS_COPY_STATUS_RESHELVING );
1791 $self->checkin_changed(1);
1796 # Returns true if the item is at the current location
1797 # because it was transited there for a hold and the
1798 # hold has not been fulfilled
1799 sub checkin_check_holds_shelf {
1801 return 0 unless $self->copy;
1804 $U->copy_status($self->copy->status)->id ==
1805 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1807 # find the hold that put us on the holds shelf
1808 my $holds = $self->editor->search_action_hold_request(
1810 current_copy => $self->copy->id,
1811 capture_time => { '!=' => undef },
1812 fulfillment_time => undef,
1813 cancel_time => undef,
1818 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1819 $self->reshelve_copy(1);
1823 my $hold = $$holds[0];
1825 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1826 $hold->id. "] for copy ".$self->copy->barcode);
1828 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1829 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1833 $logger->info("circulator: hold is not for here..");
1834 $self->remote_hold($hold);
1839 sub checkin_handle_precat {
1841 my $copy = $self->copy;
1843 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1844 $copy->status(OILS_COPY_STATUS_CATALOGING);
1845 $self->update_copy();
1846 $self->checkin_changed(1);
1847 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1852 sub checkin_build_copy_transit {
1855 my $copy = $self->copy;
1856 my $transit = Fieldmapper::action::transit_copy->new;
1858 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1859 $logger->info("circulator: transiting copy to $dest");
1861 $transit->source($self->editor->requestor->ws_ou);
1862 $transit->dest($dest);
1863 $transit->target_copy($copy->id);
1864 $transit->source_send_time('now');
1865 $transit->copy_status( $U->copy_status($copy->status)->id );
1867 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1869 return $self->bail_on_events($self->editor->event)
1870 unless $self->editor->create_action_transit_copy($transit);
1872 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1874 $self->checkin_changed(1);
1878 # returns true if the item was used (or may potentially be used
1879 # in subsequent calls) to capture a hold.
1880 sub attempt_checkin_hold_capture {
1882 my $copy = $self->copy;
1884 # we've been explicitly told not to capture any holds
1885 return 0 if $self->capture eq 'nocapture';
1887 # See if this copy can fulfill any holds
1888 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1889 $self->editor, $copy, $self->editor->requestor );
1892 $logger->debug("circulator: no potential permitted".
1893 "holds found for copy ".$copy->barcode);
1897 if($self->capture ne 'capture') {
1898 # see if this item is in a hold-capture-delay location
1899 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
1900 if($U->is_true($location->hold_verify)) {
1901 $self->bail_on_events(OpenILS::Event->new('HOLD_CAPTURE_DELAYED'));
1906 $self->retarget($retarget);
1908 $logger->info("circulator: found permitted hold ".
1909 $hold->id . " for copy, capturing...");
1911 $hold->current_copy($copy->id);
1912 $hold->capture_time('now');
1914 # prevent DB errors caused by fetching
1915 # holds from storage, and updating through cstore
1916 $hold->clear_fulfillment_time;
1917 $hold->clear_fulfillment_staff;
1918 $hold->clear_fulfillment_lib;
1919 $hold->clear_expire_time;
1920 $hold->clear_cancel_time;
1921 $hold->clear_prev_check_time unless $hold->prev_check_time;
1923 $self->bail_on_events($self->editor->event)
1924 unless $self->editor->update_action_hold_request($hold);
1926 $self->checkin_changed(1);
1928 return 0 if $self->bail_out;
1930 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1932 # This hold was captured in the correct location
1933 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1934 $self->push_events(OpenILS::Event->new('SUCCESS'));
1936 #$self->do_hold_notify($hold->id);
1937 $self->notify_hold($hold->id);
1941 # Hold needs to be picked up elsewhere. Build a hold
1942 # transit and route the item.
1943 $self->checkin_build_hold_transit();
1944 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1945 return 0 if $self->bail_out;
1946 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1949 # make sure we save the copy status
1954 sub do_hold_notify {
1955 my( $self, $holdid ) = @_;
1957 $logger->info("circulator: running delayed hold notify process");
1959 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1960 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1962 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1963 hold_id => $holdid, requestor => $self->editor->requestor);
1965 $logger->debug("circulator: built hold notifier");
1967 if(!$notifier->event) {
1969 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1971 my $stat = $notifier->send_email_notify;
1972 if( $stat == '1' ) {
1973 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1977 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1980 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1984 sub retarget_holds {
1985 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1986 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1987 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1988 # no reason to wait for the return value
1992 sub checkin_build_hold_transit {
1995 my $copy = $self->copy;
1996 my $hold = $self->hold;
1997 my $trans = Fieldmapper::action::hold_transit_copy->new;
1999 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2001 $trans->hold($hold->id);
2002 $trans->source($self->editor->requestor->ws_ou);
2003 $trans->dest($hold->pickup_lib);
2004 $trans->source_send_time("now");
2005 $trans->target_copy($copy->id);
2007 # when the copy gets to its destination, it will recover
2008 # this status - put it onto the holds shelf
2009 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2011 return $self->bail_on_events($self->editor->event)
2012 unless $self->editor->create_action_hold_transit_copy($trans);
2017 sub process_received_transit {
2019 my $copy = $self->copy;
2020 my $copyid = $self->copy->id;
2022 my $status_name = $U->copy_status($copy->status)->name;
2023 $logger->debug("circulator: attempting transit receive on ".
2024 "copy $copyid. Copy status is $status_name");
2026 my $transit = $self->transit;
2028 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2029 # - this item is in-transit to a different location
2031 my $tid = $transit->id;
2032 my $loc = $self->editor->requestor->ws_ou;
2033 my $dest = $transit->dest;
2035 $logger->info("circulator: Fowarding transit on copy which is destined ".
2036 "for a different location. transit=$tid, copy=$copyid, current ".
2037 "location=$loc, destination location=$dest");
2039 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2041 # grab the associated hold object if available
2042 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2043 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2045 return $self->bail_on_events($evt);
2048 # The transit is received, set the receive time
2049 $transit->dest_recv_time('now');
2050 $self->bail_on_events($self->editor->event)
2051 unless $self->editor->update_action_transit_copy($transit);
2053 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2055 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2056 $copy->status( $transit->copy_status );
2057 $self->update_copy();
2058 return if $self->bail_out;
2062 #$self->do_hold_notify($hold_transit->hold);
2063 $self->notify_hold($hold_transit->hold);
2068 OpenILS::Event->new(
2071 payload => { transit => $transit, holdtransit => $hold_transit } ));
2073 return $hold_transit;
2077 sub checkin_handle_circ {
2081 my $circ = $self->circ;
2082 my $copy = $self->copy;
2086 # backdate the circ if necessary
2087 if($self->backdate) {
2088 $self->checkin_handle_backdate;
2089 return if $self->bail_out;
2092 if(!$circ->stop_fines) {
2093 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2094 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2095 $circ->stop_fines_time('now') unless $self->backdate;
2096 $circ->stop_fines_time($self->backdate) if $self->backdate;
2099 # see if there are any fines owed on this circ. if not, close it
2100 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2101 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2103 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2105 # Set the checkin vars since we have the item
2106 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2108 $circ->checkin_staff($self->editor->requestor->id);
2109 $circ->checkin_lib($self->editor->requestor->ws_ou);
2111 my $circ_lib = (ref $self->copy->circ_lib) ?
2112 $self->copy->circ_lib->id : $self->copy->circ_lib;
2113 my $stat = $U->copy_status($self->copy->status)->id;
2115 # If the item is lost/missing and it needs to be sent home, don't
2116 # reshelve the copy, leave it lost/missing so the recipient will know
2117 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2118 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2119 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2122 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2126 return $self->bail_on_events($self->editor->event)
2127 unless $self->editor->update_action_circulation($circ);
2131 sub checkin_handle_backdate {
2134 my $bd = $self->backdate;
2136 # ------------------------------------------------------------------
2137 # clean up the backdate for date comparison
2138 # we want any bills created on or after the backdate
2139 # ------------------------------------------------------------------
2140 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2141 #$bd = "${bd}T23:59:59";
2143 my $bills = $self->editor->search_money_billing(
2145 billing_ts => { '>=' => $bd },
2146 xact => $self->circ->id,
2147 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2151 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2153 for my $bill (@$bills) {
2154 unless( $U->is_true($bill->voided) ) {
2155 $logger->info("backdate voiding bill ".$bill->id);
2157 $bill->void_time('now');
2158 $bill->voider($self->editor->requestor->id);
2159 my $n = $bill->note || "";
2160 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2162 $self->bail_on_events($self->editor->event)
2163 unless $self->editor->update_money_billing($bill);
2171 sub find_patron_from_copy {
2173 my $circs = $self->editor->search_action_circulation(
2174 { target_copy => $self->copy->id, checkin_time => undef });
2175 my $circ = $circs->[0];
2176 return unless $circ;
2177 my $u = $self->editor->retrieve_actor_user($circ->usr)
2178 or return $self->bail_on_events($self->editor->event);
2182 sub check_checkin_copy_status {
2184 my $copy = $self->copy;
2190 my $status = $U->copy_status($copy->status)->id;
2193 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2194 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2195 $status == OILS_COPY_STATUS_IN_PROCESS ||
2196 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2197 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2198 $status == OILS_COPY_STATUS_CATALOGING ||
2199 $status == OILS_COPY_STATUS_RESHELVING );
2201 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2202 if( $status == OILS_COPY_STATUS_LOST );
2204 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2205 if( $status == OILS_COPY_STATUS_MISSING );
2207 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2212 # --------------------------------------------------------------------------
2213 # On checkin, we need to return as many relevant objects as we can
2214 # --------------------------------------------------------------------------
2215 sub checkin_flesh_events {
2218 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2219 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2220 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2224 for my $evt (@{$self->events}) {
2227 $payload->{copy} = $U->unflesh_copy($self->copy);
2228 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2229 $payload->{circ} = $self->circ;
2230 $payload->{transit} = $self->transit;
2231 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2233 # $self->hold may or may not have been replaced with a
2234 # valid hold after processing a cancelled hold
2235 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2237 $evt->{payload} = $payload;
2242 my( $self, $msg ) = @_;
2243 my $bc = ($self->copy) ? $self->copy->barcode :
2246 my $usr = ($self->patron) ? $self->patron->id : "";
2247 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2248 ", recipient=$usr, copy=$bc");
2254 $self->log_me("do_renew()");
2255 $self->is_renewal(1);
2257 # Make sure there is an open circ to renew that is not
2258 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2259 my $circ = $self->editor->search_action_circulation(
2260 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2263 $circ = $self->editor->search_action_circulation(
2265 target_copy => $self->copy->id,
2266 stop_fines => OILS_STOP_FINES_MAX_FINES,
2267 checkin_time => undef
2272 return $self->bail_on_events($self->editor->event) unless $circ;
2274 # A user is not allowed to renew another user's items without permission
2275 unless( $circ->usr eq $self->editor->requestor->id ) {
2276 return $self->bail_on_events($self->editor->events)
2277 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2280 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2281 if $circ->renewal_remaining < 1;
2283 # -----------------------------------------------------------------
2285 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2288 $self->run_renew_permit;
2291 $self->do_checkin();
2292 return if $self->bail_out;
2294 unless( $self->permit_override ) {
2296 return if $self->bail_out;
2297 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2298 $self->remove_event('ITEM_NOT_CATALOGED');
2301 $self->override_events;
2302 return if $self->bail_out;
2305 $self->do_checkout();
2310 my( $self, $evt ) = @_;
2311 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2312 $logger->debug("circulator: removing event from list: $evt");
2313 my @events = @{$self->events};
2314 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2319 my( $self, $evt ) = @_;
2320 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2321 return grep { $_->{textcode} eq $evt } @{$self->events};
2326 sub run_renew_permit {
2331 if(!$self->legacy_script_support) {
2332 my $results = $self->run_indb_circ_test;
2333 unless($self->circ_test_success) {
2334 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2339 my $runner = $self->script_runner;
2341 $runner->load($self->circ_permit_renew);
2342 my $result = $runner->run or
2343 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2344 my $events = $result->{events};
2347 $logger->activity("ciculator: circ_permit_renew for user ".
2348 $self->patron->id." returned events: @$events") if @$events;
2350 $self->push_events(OpenILS::Event->new($_)) for @$events;
2352 $logger->debug("circulator: re-creating script runner to be safe");
2353 #$self->mk_script_runner;