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->circ_lib, '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 # requesting a precat checkout implies that any required
198 # overrides have been performed. Go ahead and re-override.
199 $circulator->override(1) if $circulator->request_precat;
200 $circulator->do_permit();
201 unless( $circulator->bail_out ) {
202 $circulator->events([]);
203 $circulator->do_checkout();
206 } elsif( $api =~ /inspect/ ) {
207 my $data = $circulator->do_inspect();
208 $circulator->editor->rollback;
211 } elsif( $api =~ /checkout/ ) {
212 $circulator->do_checkout();
214 } elsif( $api =~ /checkin/ ) {
215 $circulator->do_checkin();
217 } elsif( $api =~ /renew/ ) {
218 $circulator->is_renewal(1);
219 $circulator->do_renew();
222 if( $circulator->bail_out ) {
225 # make sure no success event accidentally slip in
227 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
230 my @e = @{$circulator->events};
231 push( @ee, $_->{textcode} ) for @e;
232 $logger->info("circulator: bailing out with events: @ee");
234 $circulator->editor->rollback;
237 $circulator->editor->commit;
240 $circulator->script_runner->cleanup if $circulator->script_runner;
242 $conn->respond_complete(circ_events($circulator));
244 unless($circulator->bail_out) {
245 $circulator->do_hold_notify($circulator->notify_hold)
246 if $circulator->notify_hold;
247 $circulator->retarget_holds if $circulator->retarget;
253 my @e = @{$circ->events};
254 # if we have multiple events, SUCCESS should not be one of them;
255 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
256 return (@e == 1) ? $e[0] : \@e;
260 sub translate_legacy_args {
263 if( $$args{barcode} ) {
264 $$args{copy_barcode} = $$args{barcode};
265 delete $$args{barcode};
268 if( $$args{copyid} ) {
269 $$args{copy_id} = $$args{copyid};
270 delete $$args{copyid};
273 if( $$args{patronid} ) {
274 $$args{patron_id} = $$args{patronid};
275 delete $$args{patronid};
278 if( $$args{patron} and !ref($$args{patron}) ) {
279 $$args{patron_id} = $$args{patron};
280 delete $$args{patron};
284 if( $$args{noncat} ) {
285 $$args{is_noncat} = $$args{noncat};
286 delete $$args{noncat};
289 if( $$args{precat} ) {
290 $$args{is_precat} = $$args{request_precat} = $$args{precat};
291 delete $$args{precat};
297 # --------------------------------------------------------------------------
298 # This package actually manages all of the circulation logic
299 # --------------------------------------------------------------------------
300 package OpenILS::Application::Circ::Circulator;
301 use strict; use warnings;
302 use vars q/$AUTOLOAD/;
304 use OpenILS::Utils::Fieldmapper;
305 use OpenSRF::Utils::Cache;
306 use Digest::MD5 qw(md5_hex);
307 use DateTime::Format::ISO8601;
308 use OpenILS::Utils::PermitHold;
309 use OpenSRF::Utils qw/:datetime/;
310 use OpenSRF::Utils::SettingsClient;
311 use OpenILS::Application::Circ::Holds;
312 use OpenILS::Application::Circ::Transit;
313 use OpenSRF::Utils::Logger qw(:logger);
314 use OpenILS::Utils::CStoreEditor qw/:funcs/;
315 use OpenILS::Application::Circ::ScriptBuilder;
316 use OpenILS::Const qw/:const/;
318 my $holdcode = "OpenILS::Application::Circ::Holds";
319 my $transcode = "OpenILS::Application::Circ::Transit";
324 # --------------------------------------------------------------------------
325 # Add a pile of automagic getter/setter methods
326 # --------------------------------------------------------------------------
327 my @AUTOLOAD_FIELDS = qw/
342 check_penalty_on_renew
370 recurring_fines_level
383 cancelled_hold_transit
392 legacy_script_support
404 my $type = ref($self) or die "$self is not an object";
406 my $name = $AUTOLOAD;
409 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
410 $logger->error("circulator: $type: invalid autoload field: $name");
411 die "$type: invalid autoload field: $name\n"
416 *{"${type}::${name}"} = sub {
419 $s->{$name} = $v if defined $v;
423 return $self->$name($data);
428 my( $class, $auth, %args ) = @_;
429 $class = ref($class) || $class;
430 my $self = bless( {}, $class );
433 $self->editor(new_editor(xact => 1, authtoken => $auth));
435 unless( $self->editor->checkauth ) {
436 $self->bail_on_events($self->editor->event);
440 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
442 $self->$_($args{$_}) for keys %args;
445 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
447 # if this is a renewal, default to desk_renewal
448 $self->desk_renewal(1) unless
449 $self->opac_renewal or $self->phone_renewal;
451 $self->capture('') unless $self->capture;
457 # --------------------------------------------------------------------------
458 # True if we should discontinue processing
459 # --------------------------------------------------------------------------
461 my( $self, $bool ) = @_;
462 if( defined $bool ) {
463 $logger->info("circulator: BAILING OUT") if $bool;
464 $self->{bail_out} = $bool;
466 return $self->{bail_out};
471 my( $self, @evts ) = @_;
474 $logger->info("circulator: pushing event ".$e->{textcode});
475 push( @{$self->events}, $e ) unless
476 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
482 my $key = md5_hex( time() . rand() . "$$" );
483 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
484 return $self->permit_key($key);
487 sub check_permit_key {
489 my $key = $self->permit_key;
490 return 0 unless $key;
491 my $k = "oils_permit_key_$key";
492 my $one = $self->cache_handle->get_cache($k);
493 $self->cache_handle->delete_cache($k);
494 return ($one) ? 1 : 0;
499 my $e = $self->editor;
501 # --------------------------------------------------------------------------
502 # Grab the fleshed copy
503 # --------------------------------------------------------------------------
504 unless($self->is_noncat) {
508 flesh_fields => {acp => ['call_number'], acn => ['record']}
511 $copy = $e->retrieve_asset_copy(
512 [$self->copy_id, $flesh ]) or return $e->event;
514 } elsif( $self->copy_barcode ) {
516 $copy = $e->search_asset_copy(
517 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
522 $self->volume($copy->call_number);
523 $self->title($self->volume->record);
524 $self->copy->call_number($self->volume->id);
525 $self->volume->record($self->title->id);
526 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
527 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
528 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
529 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
532 # We can't renew if there is no copy
533 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
534 if $self->is_renewal;
539 return undef if $self->is_checkin;
541 # --------------------------------------------------------------------------
543 # --------------------------------------------------------------------------
545 if( $self->patron_id ) {
546 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
548 } elsif( $self->patron_barcode ) {
550 my $card = $e->search_actor_card(
551 {barcode => $self->patron_barcode})->[0] or return $e->event;
553 $patron = $e->search_actor_user(
554 {card => $card->id})->[0] or return $e->event;
557 if( my $copy = $self->copy ) {
558 my $circs = $e->search_action_circulation(
559 {target_copy => $copy->id, checkin_time => undef});
561 if( my $circ = $circs->[0] ) {
562 $patron = $e->retrieve_actor_user($circ->usr)
568 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
569 unless $self->patron($patron);
572 # --------------------------------------------------------------------------
573 # This builds the script runner environment and fetches most of the
575 # --------------------------------------------------------------------------
576 sub mk_script_runner {
582 qw/copy copy_barcode copy_id patron
583 patron_id patron_barcode volume title editor/;
585 # Translate our objects into the ScriptBuilder args hash
586 $$args{$_} = $self->$_() for @fields;
588 $args->{ignore_user_status} = 1 if $self->is_checkin;
589 $$args{fetch_patron_by_circ_copy} = 1;
590 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
592 if( my $pco = $self->pending_checkouts ) {
593 $logger->info("circulator: we were given a pending checkouts number of $pco");
594 $$args{patronItemsOut} = $pco;
597 # This fetches most of the objects we need
598 $self->script_runner(
599 OpenILS::Application::Circ::ScriptBuilder->build($args));
601 # Now we translate the ScriptBuilder objects back into self
602 $self->$_($$args{$_}) for @fields;
604 my @evts = @{$args->{_events}} if $args->{_events};
606 $logger->debug("circulator: script builder returned events: @evts") if @evts;
610 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
611 if(!$self->is_noncat and
613 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
617 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
618 return $self->bail_on_events(@e);
623 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
624 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
625 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
626 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
630 # We can't renew if there is no copy
631 return $self->bail_on_events(@evts) if
632 $self->is_renewal and !$self->copy;
634 # Set some circ-specific flags in the script environment
635 my $evt = "environment";
636 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
638 if( $self->is_noncat ) {
639 $self->script_runner->insert("$evt.isNonCat", 1);
640 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
643 if( $self->is_precat ) {
644 $self->script_runner->insert("environment.isPrecat", 1, 1);
647 $self->script_runner->add_path( $_ ) for @$script_libs;
652 # --------------------------------------------------------------------------
653 # Does the circ permit work
654 # --------------------------------------------------------------------------
658 $self->log_me("do_permit()");
660 unless( $self->editor->requestor->id == $self->patron->id ) {
661 return $self->bail_on_events($self->editor->event)
662 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
665 $self->check_captured_holds();
666 $self->do_copy_checks();
667 return if $self->bail_out;
668 $self->run_patron_permit_scripts();
669 $self->run_copy_permit_scripts()
670 unless $self->is_precat or $self->is_noncat;
671 $self->check_item_deposit_events();
672 $self->override_events() unless
673 $self->is_renewal and not $self->check_penalty_on_renew;
674 return if $self->bail_out;
676 if($self->is_precat and not $self->request_precat) {
679 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
680 return $self->bail_out(1) unless $self->is_renewal;
686 payload => $self->mk_permit_key));
689 sub check_item_deposit_events {
691 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
692 if $self->is_deposit and not $self->is_deposit_exempt;
693 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
694 if $self->is_rental and not $self->is_rental_exempt;
697 # returns true if the user is not required to pay deposits
698 sub is_deposit_exempt {
700 my $pid = (ref $self->patron->profile) ?
701 $self->patron->profile->id : $self->patron->profile;
702 my $groups = $U->ou_ancestor_setting_value(
703 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
704 return 1 if $groups and grep {$_ == $pid} @$groups;
708 # returns true if the user is not required to pay rental fees
709 sub is_rental_exempt {
711 my $pid = (ref $self->patron->profile) ?
712 $self->patron->profile->id : $self->patron->profile;
713 my $groups = $U->ou_ancestor_setting_value(
714 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
715 return 1 if $groups and grep {$_ == $pid} @$groups;
719 sub check_captured_holds {
721 my $copy = $self->copy;
722 my $patron = $self->patron;
724 return undef unless $copy;
726 my $s = $U->copy_status($copy->status)->id;
727 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
728 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
730 # Item is on the holds shelf, make sure it's going to the right person
731 my $holds = $self->editor->search_action_hold_request(
734 current_copy => $copy->id ,
735 capture_time => { '!=' => undef },
736 cancel_time => undef,
737 fulfillment_time => undef
743 if( $holds and $$holds[0] ) {
744 return undef if $$holds[0]->usr == $patron->id;
747 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
749 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
755 my $copy = $self->copy;
758 my $stat = $U->copy_status($copy->status)->id;
760 # We cannot check out a copy if it is in-transit
761 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
762 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
765 $self->handle_claims_returned();
766 return if $self->bail_out;
768 # no claims returned circ was found, check if there is any open circ
769 unless( $self->is_renewal ) {
770 my $circs = $self->editor->search_action_circulation(
771 { target_copy => $copy->id, checkin_time => undef }
774 return $self->bail_on_events(
775 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
780 sub send_penalty_request {
782 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
783 $self->penalty_request(
785 'open-ils.penalty.patron_penalty.calculate',
787 authtoken => $self->editor->authtoken,
788 patron => $self->patron } ) );
791 sub gather_penalty_request {
793 return [] unless $self->penalty_request;
794 my $data = $self->penalty_request->recv;
796 throw $data if UNIVERSAL::isa($data,'Error');
797 $data = $data->content;
798 return $data->{fatal_penalties};
800 $logger->error("circulator: penalty request returned no data");
804 my $LEGACY_CIRC_EVENT_MAP = {
805 'actor.usr.barred' => 'PATRON_BARRED',
806 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
807 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
808 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
809 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
810 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
811 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
812 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
813 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
817 # ---------------------------------------------------------------------
818 # This pushes any patron-related events into the list but does not
819 # set bail_out for any events
820 # ---------------------------------------------------------------------
821 sub run_patron_permit_scripts {
823 my $runner = $self->script_runner;
824 my $patronid = $self->patron->id;
828 if(!$self->legacy_script_support) {
830 my $results = $self->run_indb_circ_test;
831 unless($self->circ_test_success) {
832 push(@allevents, OpenILS::Event->new(
833 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
838 $self->send_penalty_request() unless
839 $self->is_renewal and not $self->check_penalty_on_renew;
842 # --------------------------------------------------------------------- # Now run the patron permit script
843 # ---------------------------------------------------------------------
844 $runner->load($self->circ_permit_patron);
845 my $result = $runner->run or
846 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
848 my $patron_events = $result->{events};
850 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
851 [] : $self->gather_penalty_request();
853 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
856 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
858 $self->push_events(@allevents);
861 sub run_indb_circ_test {
863 return $self->matrix_test_result if $self->matrix_test_result;
865 my $dbfunc = ($self->is_renewal) ?
866 'action.item_user_renew_test' : 'action.item_user_circ_test';
868 my $results = ($self->matrix_test_result) ?
869 $self->matrix_test_result :
870 $self->editor->json_query(
873 $self->editor->requestor->ws_ou,
874 ($self->is_precat) ? undef : $self->copy->id,
880 $self->circ_test_success($U->is_true($results->[0]->{success}));
881 if($self->circ_test_success) {
882 $self->circ_matrix_test(
883 $self->editor->retrieve_config_circ_matrix_test(
884 $results->[0]->{matchpoint}
889 if($self->circ_test_success) {
890 $self->circ_matrix_ruleset(
891 $self->editor->retrieve_config_circ_matrix_ruleset([
892 $results->[0]->{matchpoint},
895 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
903 return $self->matrix_test_result($results);
908 $self->run_indb_circ_test;
911 circ_test_success => $self->circ_test_success,
912 failure_events => [],
916 unless($self->circ_test_success) {
917 push(@{$results->{failure_codes}},
918 $_->{fail_part}) for @{$self->matrix_test_result};
919 push(@{$results->{failure_events}},
920 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
921 for @{$self->matrix_test_result};
925 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
926 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
927 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
929 my $policy = $self->get_circ_policy(
930 $duration_rule, $recurring_fine_rule, $max_fine_rule);
932 $$results{$_} = $$policy{$_} for keys %$policy;
936 # ---------------------------------------------------------------------
937 # Loads the circ policy info for duration, recurring fine, and max
938 # fine based on the current copy
939 # ---------------------------------------------------------------------
940 sub get_circ_policy {
941 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
944 duration_rule => $duration_rule->name,
945 recurring_fine_rule => $recurring_fine_rule->name,
946 max_fine_rule => $max_fine_rule->name,
947 max_fine => $self->get_max_fine_amount($max_fine_rule),
948 fine_interval => $recurring_fine_rule->recurance_interval,
949 renewal_remaining => $duration_rule->max_renewals
952 $policy->{duration} = $duration_rule->shrt
953 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
954 $policy->{duration} = $duration_rule->normal
955 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
956 $policy->{duration} = $duration_rule->extended
957 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
959 $policy->{recurring_fine} = $recurring_fine_rule->low
960 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
961 $policy->{recurring_fine} = $recurring_fine_rule->normal
962 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
963 $policy->{recurring_fine} = $recurring_fine_rule->high
964 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
969 sub get_max_fine_amount {
971 my $max_fine_rule = shift;
972 my $max_amount = $max_fine_rule->amount;
974 # if is_percent is true then the max->amount is
975 # use as a percentage of the copy price
976 if ($U->is_true($max_fine_rule->is_percent)) {
977 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
978 $max_amount = $price * $max_fine_rule->amount / 100;
986 sub run_copy_permit_scripts {
988 my $copy = $self->copy || return;
989 my $runner = $self->script_runner;
993 if(!$self->legacy_script_support) {
994 my $results = $self->run_indb_circ_test;
995 unless($self->circ_test_success) {
996 push(@allevents, OpenILS::Event->new(
997 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
1001 # ---------------------------------------------------------------------
1002 # Capture all of the copy permit events
1003 # ---------------------------------------------------------------------
1004 $runner->load($self->circ_permit_copy);
1005 my $result = $runner->run or
1006 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1007 my $copy_events = $result->{events};
1009 # ---------------------------------------------------------------------
1010 # Now collect all of the events together
1011 # ---------------------------------------------------------------------
1012 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1015 # See if this copy has an alert message
1016 my $ae = $self->check_copy_alert();
1017 push( @allevents, $ae ) if $ae;
1019 # uniquify the events
1020 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1021 @allevents = values %hash;
1024 $_->{payload} = $copy if
1025 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1028 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1030 $self->push_events(@allevents);
1034 sub check_copy_alert {
1036 return undef if $self->is_renewal;
1037 return OpenILS::Event->new(
1038 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1039 if $self->copy and $self->copy->alert_message;
1045 # --------------------------------------------------------------------------
1046 # If the call is overriding and has permissions to override every collected
1047 # event, the are cleared. Any event that the caller does not have
1048 # permission to override, will be left in the event list and bail_out will
1050 # XXX We need code in here to cancel any holds/transits on copies
1051 # that are being force-checked out
1052 # --------------------------------------------------------------------------
1053 sub override_events {
1055 my @events = @{$self->events};
1056 return unless @events;
1058 if(!$self->override) {
1059 return $self->bail_out(1)
1060 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1065 for my $e (@events) {
1066 my $tc = $e->{textcode};
1067 next if $tc eq 'SUCCESS';
1068 my $ov = "$tc.override";
1069 $logger->info("circulator: attempting to override event: $ov");
1071 return $self->bail_on_events($self->editor->event)
1072 unless( $self->editor->allowed($ov) );
1077 # --------------------------------------------------------------------------
1078 # If there is an open claimsreturn circ on the requested copy, close the
1079 # circ if overriding, otherwise bail out
1080 # --------------------------------------------------------------------------
1081 sub handle_claims_returned {
1083 my $copy = $self->copy;
1085 my $CR = $self->editor->search_action_circulation(
1087 target_copy => $copy->id,
1088 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1089 checkin_time => undef,
1093 return unless ($CR = $CR->[0]);
1097 # - If the caller has set the override flag, we will check the item in
1098 if($self->override) {
1100 $CR->checkin_time('now');
1101 $CR->checkin_lib($self->editor->requestor->ws_ou);
1102 $CR->checkin_staff($self->editor->requestor->id);
1104 $evt = $self->editor->event
1105 unless $self->editor->update_action_circulation($CR);
1108 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1111 $self->bail_on_events($evt) if $evt;
1116 # --------------------------------------------------------------------------
1117 # This performs the checkout
1118 # --------------------------------------------------------------------------
1122 $self->log_me("do_checkout()");
1124 # make sure perms are good if this isn't a renewal
1125 unless( $self->is_renewal ) {
1126 return $self->bail_on_events($self->editor->event)
1127 unless( $self->editor->allowed('COPY_CHECKOUT') );
1130 # verify the permit key
1131 unless( $self->check_permit_key ) {
1132 if( $self->permit_override ) {
1133 return $self->bail_on_events($self->editor->event)
1134 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1136 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1140 # if this is a non-cataloged circ, build the circ and finish
1141 if( $self->is_noncat ) {
1142 $self->checkout_noncat;
1144 OpenILS::Event->new('SUCCESS',
1145 payload => { noncat_circ => $self->circ }));
1149 if( $self->is_precat ) {
1150 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1151 $self->make_precat_copy;
1152 return if $self->bail_out;
1154 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1155 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1158 $self->do_copy_checks;
1159 return if $self->bail_out;
1161 $self->run_checkout_scripts();
1162 return if $self->bail_out;
1164 $self->build_checkout_circ_object();
1165 return if $self->bail_out;
1167 $self->apply_modified_due_date();
1168 return if $self->bail_out;
1170 return $self->bail_on_events($self->editor->event)
1171 unless $self->editor->create_action_circulation($self->circ);
1173 # refresh the circ to force local time zone for now
1174 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1176 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1178 return if $self->bail_out;
1180 $self->apply_deposit_fee();
1181 return if $self->bail_out;
1183 $self->handle_checkout_holds();
1184 return if $self->bail_out;
1186 # ------------------------------------------------------------------------------
1187 # Update the patron penalty info in the DB. Run it for permit-overrides or
1188 # renewals since both of those cases do not require the penalty server to
1189 # run during the permit phase of the checkout
1190 # ------------------------------------------------------------------------------
1191 if( $self->permit_override or $self->is_renewal ) {
1192 $U->update_patron_penalties(
1193 authtoken => $self->editor->authtoken,
1194 patron => $self->patron,
1199 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1201 OpenILS::Event->new('SUCCESS',
1203 copy => $U->unflesh_copy($self->copy),
1204 circ => $self->circ,
1206 holds_fulfilled => $self->fulfilled_holds,
1207 deposit_billing => $self->deposit_billing,
1208 rental_billing => $self->rental_billing
1214 sub apply_deposit_fee {
1216 my $copy = $self->copy;
1218 ($self->is_deposit and not $self->is_deposit_exempt) or
1219 ($self->is_rental and not $self->is_rental_exempt);
1221 my $bill = Fieldmapper::money::billing->new;
1222 my $amount = $copy->deposit_amount;
1225 if($self->is_deposit) {
1226 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1227 $self->deposit_billing($bill);
1229 $billing_type = OILS_BILLING_TYPE_RENTAL;
1230 $self->rental_billing($bill);
1233 $bill->xact($self->circ->id);
1234 $bill->amount($amount);
1235 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1236 $bill->billing_type($billing_type);
1237 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1239 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1244 my $copy = $self->copy;
1246 my $stat = $copy->status if ref $copy->status;
1247 my $loc = $copy->location if ref $copy->location;
1248 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1250 $copy->status($stat->id) if $stat;
1251 $copy->location($loc->id) if $loc;
1252 $copy->circ_lib($circ_lib->id) if $circ_lib;
1253 $copy->editor($self->editor->requestor->id);
1254 $copy->edit_date('now');
1255 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1257 return $self->bail_on_events($self->editor->event)
1258 unless $self->editor->update_asset_copy($self->copy);
1260 $copy->status($U->copy_status($copy->status));
1261 $copy->location($loc) if $loc;
1262 $copy->circ_lib($circ_lib) if $circ_lib;
1266 sub bail_on_events {
1267 my( $self, @evts ) = @_;
1268 $self->push_events(@evts);
1272 sub handle_checkout_holds {
1275 my $copy = $self->copy;
1276 my $patron = $self->patron;
1278 my $holds = $self->editor->search_action_hold_request(
1280 current_copy => $copy->id ,
1281 cancel_time => undef,
1282 fulfillment_time => undef
1288 # XXX We should only fulfill one hold here...
1289 # XXX If a hold was transited to the user who is checking out
1290 # the item, we need to make sure that hold is what's grabbed
1293 # for now, just sort by id to get what should be the oldest hold
1294 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1295 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1296 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1299 my $hold = $myholds[0];
1301 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1303 # if the hold was never officially captured, capture it.
1304 $hold->capture_time('now') unless $hold->capture_time;
1306 # just make sure it's set correctly
1307 $hold->current_copy($copy->id);
1309 $hold->fulfillment_time('now');
1310 $hold->fulfillment_staff($self->editor->requestor->id);
1311 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1313 return $self->bail_on_events($self->editor->event)
1314 unless $self->editor->update_action_hold_request($hold);
1316 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1318 push( @fulfilled, $hold->id );
1321 # If there are any holds placed for other users that point to this copy,
1322 # then we need to un-target those holds so the targeter can pick a new copy
1325 $logger->info("circulator: un-targeting hold ".$_->id.
1326 " because copy ".$copy->id." is getting checked out");
1328 # - make the targeter process this hold at next run
1329 $_->clear_prev_check_time;
1331 # - clear out the targetted copy
1332 $_->clear_current_copy;
1333 $_->clear_capture_time;
1335 return $self->bail_on_event($self->editor->event)
1336 unless $self->editor->update_action_hold_request($_);
1340 $self->fulfilled_holds(\@fulfilled);
1345 sub run_checkout_scripts {
1349 my $runner = $self->script_runner;
1358 if(!$self->legacy_script_support) {
1359 $self->run_indb_circ_test();
1360 $duration = $self->circ_matrix_ruleset->duration_rule;
1361 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1362 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1366 $runner->load($self->circ_duration);
1368 my $result = $runner->run or
1369 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1371 $duration_name = $result->{durationRule};
1372 $recurring_name = $result->{recurringFinesRule};
1373 $max_fine_name = $result->{maxFine};
1376 $duration_name = $duration->name if $duration;
1377 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1380 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1381 return $self->bail_on_events($evt) if $evt;
1383 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1384 return $self->bail_on_events($evt) if $evt;
1386 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1387 return $self->bail_on_events($evt) if $evt;
1392 # The item circulates with an unlimited duration
1398 $self->duration_rule($duration);
1399 $self->recurring_fines_rule($recurring);
1400 $self->max_fine_rule($max_fine);
1404 sub build_checkout_circ_object {
1407 my $circ = Fieldmapper::action::circulation->new;
1408 my $duration = $self->duration_rule;
1409 my $max = $self->max_fine_rule;
1410 my $recurring = $self->recurring_fines_rule;
1411 my $copy = $self->copy;
1412 my $patron = $self->patron;
1416 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1418 my $dname = $duration->name;
1419 my $mname = $max->name;
1420 my $rname = $recurring->name;
1422 $logger->debug("circulator: building circulation ".
1423 "with duration=$dname, maxfine=$mname, recurring=$rname");
1425 $circ->duration($policy->{duration});
1426 $circ->recuring_fine($policy->{recurring_fine});
1427 $circ->duration_rule($duration->name);
1428 $circ->recuring_fine_rule($recurring->name);
1429 $circ->max_fine_rule($max->name);
1430 $circ->max_fine($policy->{max_fine});
1431 $circ->fine_interval($recurring->recurance_interval);
1432 $circ->renewal_remaining($duration->max_renewals);
1436 $logger->info("circulator: copy found with an unlimited circ duration");
1437 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1438 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1439 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1440 $circ->renewal_remaining(0);
1443 $circ->target_copy( $copy->id );
1444 $circ->usr( $patron->id );
1445 $circ->circ_lib( $self->circ_lib );
1447 if( $self->is_renewal ) {
1448 $circ->opac_renewal('t') if $self->opac_renewal;
1449 $circ->phone_renewal('t') if $self->phone_renewal;
1450 $circ->desk_renewal('t') if $self->desk_renewal;
1451 $circ->renewal_remaining($self->renewal_remaining);
1452 $circ->circ_staff($self->editor->requestor->id);
1456 # if the user provided an overiding checkout time,
1457 # (e.g. the checkout really happened several hours ago), then
1458 # we apply that here. Does this need a perm??
1459 $circ->xact_start(clense_ISO8601($self->checkout_time))
1460 if $self->checkout_time;
1462 # if a patron is renewing, 'requestor' will be the patron
1463 $circ->circ_staff($self->editor->requestor->id);
1464 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1470 sub apply_modified_due_date {
1472 my $circ = $self->circ;
1473 my $copy = $self->copy;
1475 if( $self->due_date ) {
1477 return $self->bail_on_events($self->editor->event)
1478 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1480 $circ->due_date(clense_ISO8601($self->due_date));
1484 # if the due_date lands on a day when the location is closed
1485 return unless $copy and $circ->due_date;
1487 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1489 # due-date overlap should be determined by the location the item
1490 # is checked out from, not the owning or circ lib of the item
1491 my $org = $self->editor->requestor->ws_ou;
1493 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1494 " with an item due date of ".$circ->due_date );
1496 my $dateinfo = $U->storagereq(
1497 'open-ils.storage.actor.org_unit.closed_date.overlap',
1498 $org, $circ->due_date );
1501 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1502 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1504 # XXX make the behavior more dynamic
1505 # for now, we just push the due date to after the close date
1506 $circ->due_date($dateinfo->{end});
1513 sub create_due_date {
1514 my( $self, $duration ) = @_;
1515 # if there is a raw time component (e.g. from postgres),
1516 # turn it into an interval that interval_to_seconds can parse
1517 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1518 my ($sec,$min,$hour,$mday,$mon,$year) =
1519 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1520 $year += 1900; $mon += 1;
1521 my $due_date = sprintf(
1522 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1523 $year, $mon, $mday, $hour, $min, $sec);
1529 sub make_precat_copy {
1531 my $copy = $self->copy;
1534 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1536 $copy->editor($self->editor->requestor->id);
1537 $copy->edit_date('now');
1538 $copy->dummy_title($self->dummy_title);
1539 $copy->dummy_author($self->dummy_author);
1541 $self->update_copy();
1545 $logger->info("circulator: Creating a new precataloged ".
1546 "copy in checkout with barcode " . $self->copy_barcode);
1548 $copy = Fieldmapper::asset::copy->new;
1549 $copy->circ_lib($self->circ_lib);
1550 $copy->creator($self->editor->requestor->id);
1551 $copy->editor($self->editor->requestor->id);
1552 $copy->barcode($self->copy_barcode);
1553 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1554 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1555 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1557 $copy->dummy_title($self->dummy_title || "");
1558 $copy->dummy_author($self->dummy_author || "");
1560 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1562 $self->push_events($self->editor->event);
1566 # this is a little bit of a hack, but we need to
1567 # get the copy into the script runner
1568 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1572 sub checkout_noncat {
1578 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1579 my $count = $self->noncat_count || 1;
1580 my $cotime = clense_ISO8601($self->checkout_time) || "";
1582 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1586 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1587 $self->editor->requestor->id,
1595 $self->push_events($evt);
1606 $self->log_me("do_checkin()");
1609 return $self->bail_on_events(
1610 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1613 if( $self->checkin_check_holds_shelf() ) {
1614 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1615 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1616 $self->checkin_flesh_events;
1620 unless( $self->is_renewal ) {
1621 return $self->bail_on_events($self->editor->event)
1622 unless $self->editor->allowed('COPY_CHECKIN');
1625 $self->push_events($self->check_copy_alert());
1626 $self->push_events($self->check_checkin_copy_status());
1628 # the renew code will have already found our circulation object
1629 unless( $self->is_renewal and $self->circ ) {
1630 my $circs = $self->editor->search_action_circulation(
1631 { target_copy => $self->copy->id, checkin_time => undef });
1632 $self->circ($$circs[0]);
1634 # for now, just warn if there are multiple open circs on a copy
1635 $logger->warn("circulator: we have ".scalar(@$circs).
1636 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1639 # if the circ is marked as 'claims returned', add the event to the list
1640 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1641 if ($self->circ and $self->circ->stop_fines
1642 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1644 $self->check_circ_deposit();
1646 # handle the overridable events
1647 $self->override_events unless $self->is_renewal;
1648 return if $self->bail_out;
1652 $self->editor->search_action_transit_copy(
1653 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1657 $self->checkin_handle_circ;
1658 return if $self->bail_out;
1659 $self->checkin_changed(1);
1661 } elsif( $self->transit ) {
1662 my $hold_transit = $self->process_received_transit;
1663 $self->checkin_changed(1);
1665 if( $self->bail_out ) {
1666 $self->checkin_flesh_events;
1670 if( my $e = $self->check_checkin_copy_status() ) {
1671 # If the original copy status is special, alert the caller
1672 my $ev = $self->events;
1673 $self->events([$e]);
1674 $self->override_events;
1675 return if $self->bail_out;
1679 if( $hold_transit or
1680 $U->copy_status($self->copy->status)->id
1681 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1684 if( $hold_transit ) {
1685 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1687 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1692 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1694 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1695 $self->reshelve_copy(1);
1696 $self->cancelled_hold_transit(1);
1697 $self->notify_hold(0); # don't notify for cancelled holds
1698 return if $self->bail_out;
1702 # hold transited to correct location
1703 $self->checkin_flesh_events;
1708 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1710 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1711 " that is in-transit, but there is no transit.. repairing");
1712 $self->reshelve_copy(1);
1713 return if $self->bail_out;
1716 if( $self->is_renewal ) {
1717 $self->push_events(OpenILS::Event->new('SUCCESS'));
1721 # ------------------------------------------------------------------------------
1722 # Circulations and transits are now closed where necessary. Now go on to see if
1723 # this copy can fulfill a hold or needs to be routed to a different location
1724 # ------------------------------------------------------------------------------
1726 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1728 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1729 return if $self->bail_out;
1731 unless($needed_for_hold) {
1732 my $circ_lib = (ref $self->copy->circ_lib) ?
1733 $self->copy->circ_lib->id : $self->copy->circ_lib;
1735 if( $self->remote_hold ) {
1736 $circ_lib = $self->remote_hold->pickup_lib;
1737 $logger->warn("circulator: Copy ".$self->copy->barcode.
1738 " is on a remote hold's shelf, sending to $circ_lib");
1741 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1743 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1745 $self->checkin_handle_precat();
1746 return if $self->bail_out;
1750 my $bc = $self->copy->barcode;
1751 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1752 $self->checkin_build_copy_transit($circ_lib);
1753 return if $self->bail_out;
1754 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1759 $self->reshelve_copy;
1760 return if $self->bail_out;
1762 unless($self->checkin_changed) {
1764 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1765 my $stat = $U->copy_status($self->copy->status)->id;
1767 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1768 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1769 $self->bail_out(1); # no need to commit anything
1773 $self->push_events(OpenILS::Event->new('SUCCESS'))
1774 unless @{$self->events};
1778 # ------------------------------------------------------------------------------
1779 # Update the patron penalty info in the DB
1780 # ------------------------------------------------------------------------------
1781 $U->update_patron_penalties(
1782 authtoken => $self->editor->authtoken,
1783 patron => $self->patron,
1784 background => 1 ) if $self->is_checkin;
1786 $self->checkin_flesh_events;
1790 # if a deposit was payed for this item, push the event
1791 sub check_circ_deposit {
1793 return unless $self->circ;
1794 my $deposit = $self->editor->search_money_billing(
1795 { billing_type => OILS_BILLING_TYPE_DEPOSIT,
1796 xact => $self->circ->id,
1798 }, {idlist => 1})->[0];
1800 $self->push_events(OpenILS::Event->new(
1801 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1806 my $force = $self->force || shift;
1807 my $copy = $self->copy;
1809 my $stat = $U->copy_status($copy->status)->id;
1812 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1813 $stat != OILS_COPY_STATUS_CATALOGING and
1814 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1815 $stat != OILS_COPY_STATUS_RESHELVING )) {
1817 $copy->status( OILS_COPY_STATUS_RESHELVING );
1819 $self->checkin_changed(1);
1824 # Returns true if the item is at the current location
1825 # because it was transited there for a hold and the
1826 # hold has not been fulfilled
1827 sub checkin_check_holds_shelf {
1829 return 0 unless $self->copy;
1832 $U->copy_status($self->copy->status)->id ==
1833 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1835 # find the hold that put us on the holds shelf
1836 my $holds = $self->editor->search_action_hold_request(
1838 current_copy => $self->copy->id,
1839 capture_time => { '!=' => undef },
1840 fulfillment_time => undef,
1841 cancel_time => undef,
1846 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1847 $self->reshelve_copy(1);
1851 my $hold = $$holds[0];
1853 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1854 $hold->id. "] for copy ".$self->copy->barcode);
1856 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1857 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1861 $logger->info("circulator: hold is not for here..");
1862 $self->remote_hold($hold);
1867 sub checkin_handle_precat {
1869 my $copy = $self->copy;
1871 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1872 $copy->status(OILS_COPY_STATUS_CATALOGING);
1873 $self->update_copy();
1874 $self->checkin_changed(1);
1875 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1880 sub checkin_build_copy_transit {
1883 my $copy = $self->copy;
1884 my $transit = Fieldmapper::action::transit_copy->new;
1886 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1887 $logger->info("circulator: transiting copy to $dest");
1889 $transit->source($self->editor->requestor->ws_ou);
1890 $transit->dest($dest);
1891 $transit->target_copy($copy->id);
1892 $transit->source_send_time('now');
1893 $transit->copy_status( $U->copy_status($copy->status)->id );
1895 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1897 return $self->bail_on_events($self->editor->event)
1898 unless $self->editor->create_action_transit_copy($transit);
1900 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1902 $self->checkin_changed(1);
1906 # returns true if the item was used (or may potentially be used
1907 # in subsequent calls) to capture a hold.
1908 sub attempt_checkin_hold_capture {
1910 my $copy = $self->copy;
1912 # we've been explicitly told not to capture any holds
1913 return 0 if $self->capture eq 'nocapture';
1915 # See if this copy can fulfill any holds
1916 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1917 $self->editor, $copy, $self->editor->requestor );
1920 $logger->debug("circulator: no potential permitted".
1921 "holds found for copy ".$copy->barcode);
1925 if($self->capture ne 'capture') {
1926 # see if this item is in a hold-capture-delay location
1927 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
1928 if($U->is_true($location->hold_verify)) {
1929 $self->bail_on_events(
1930 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
1935 $self->retarget($retarget);
1937 $logger->info("circulator: found permitted hold ".
1938 $hold->id . " for copy, capturing...");
1940 $hold->current_copy($copy->id);
1941 $hold->capture_time('now');
1943 # prevent DB errors caused by fetching
1944 # holds from storage, and updating through cstore
1945 $hold->clear_fulfillment_time;
1946 $hold->clear_fulfillment_staff;
1947 $hold->clear_fulfillment_lib;
1948 $hold->clear_expire_time;
1949 $hold->clear_cancel_time;
1950 $hold->clear_prev_check_time unless $hold->prev_check_time;
1952 $self->bail_on_events($self->editor->event)
1953 unless $self->editor->update_action_hold_request($hold);
1955 $self->checkin_changed(1);
1957 return 0 if $self->bail_out;
1959 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1961 # This hold was captured in the correct location
1962 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1963 $self->push_events(OpenILS::Event->new('SUCCESS'));
1965 #$self->do_hold_notify($hold->id);
1966 $self->notify_hold($hold->id);
1970 # Hold needs to be picked up elsewhere. Build a hold
1971 # transit and route the item.
1972 $self->checkin_build_hold_transit();
1973 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1974 return 0 if $self->bail_out;
1975 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1978 # make sure we save the copy status
1983 sub do_hold_notify {
1984 my( $self, $holdid ) = @_;
1986 $logger->info("circulator: running delayed hold notify process");
1988 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1989 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1991 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1992 hold_id => $holdid, requestor => $self->editor->requestor);
1994 $logger->debug("circulator: built hold notifier");
1996 if(!$notifier->event) {
1998 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
2000 my $stat = $notifier->send_email_notify;
2001 if( $stat == '1' ) {
2002 $logger->info("ciculator: hold notify succeeded for hold $holdid");
2006 $logger->warn("ciculator: * hold notify failed for hold $holdid");
2009 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
2013 sub retarget_holds {
2014 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
2015 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2016 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
2017 # no reason to wait for the return value
2021 sub checkin_build_hold_transit {
2024 my $copy = $self->copy;
2025 my $hold = $self->hold;
2026 my $trans = Fieldmapper::action::hold_transit_copy->new;
2028 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2030 $trans->hold($hold->id);
2031 $trans->source($self->editor->requestor->ws_ou);
2032 $trans->dest($hold->pickup_lib);
2033 $trans->source_send_time("now");
2034 $trans->target_copy($copy->id);
2036 # when the copy gets to its destination, it will recover
2037 # this status - put it onto the holds shelf
2038 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2040 return $self->bail_on_events($self->editor->event)
2041 unless $self->editor->create_action_hold_transit_copy($trans);
2046 sub process_received_transit {
2048 my $copy = $self->copy;
2049 my $copyid = $self->copy->id;
2051 my $status_name = $U->copy_status($copy->status)->name;
2052 $logger->debug("circulator: attempting transit receive on ".
2053 "copy $copyid. Copy status is $status_name");
2055 my $transit = $self->transit;
2057 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2058 # - this item is in-transit to a different location
2060 my $tid = $transit->id;
2061 my $loc = $self->editor->requestor->ws_ou;
2062 my $dest = $transit->dest;
2064 $logger->info("circulator: Fowarding transit on copy which is destined ".
2065 "for a different location. transit=$tid, copy=$copyid, current ".
2066 "location=$loc, destination location=$dest");
2068 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2070 # grab the associated hold object if available
2071 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2072 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2074 return $self->bail_on_events($evt);
2077 # The transit is received, set the receive time
2078 $transit->dest_recv_time('now');
2079 $self->bail_on_events($self->editor->event)
2080 unless $self->editor->update_action_transit_copy($transit);
2082 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2084 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2085 $copy->status( $transit->copy_status );
2086 $self->update_copy();
2087 return if $self->bail_out;
2091 #$self->do_hold_notify($hold_transit->hold);
2092 $self->notify_hold($hold_transit->hold);
2097 OpenILS::Event->new(
2100 payload => { transit => $transit, holdtransit => $hold_transit } ));
2102 return $hold_transit;
2106 sub checkin_handle_circ {
2110 my $circ = $self->circ;
2111 my $copy = $self->copy;
2115 # backdate the circ if necessary
2116 if($self->backdate) {
2117 $self->checkin_handle_backdate;
2118 return if $self->bail_out;
2121 if(!$circ->stop_fines) {
2122 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2123 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2124 $circ->stop_fines_time('now') unless $self->backdate;
2125 $circ->stop_fines_time($self->backdate) if $self->backdate;
2128 # see if there are any fines owed on this circ. if not, close it
2129 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2130 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2132 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2134 # Set the checkin vars since we have the item
2135 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2137 $circ->checkin_staff($self->editor->requestor->id);
2138 $circ->checkin_lib($self->editor->requestor->ws_ou);
2140 my $circ_lib = (ref $self->copy->circ_lib) ?
2141 $self->copy->circ_lib->id : $self->copy->circ_lib;
2142 my $stat = $U->copy_status($self->copy->status)->id;
2144 # If the item is lost/missing and it needs to be sent home, don't
2145 # reshelve the copy, leave it lost/missing so the recipient will know
2146 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2147 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2148 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2151 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2155 return $self->bail_on_events($self->editor->event)
2156 unless $self->editor->update_action_circulation($circ);
2160 sub checkin_handle_backdate {
2163 my $bd = $self->backdate;
2165 # ------------------------------------------------------------------
2166 # clean up the backdate for date comparison
2167 # we want any bills created on or after the backdate
2168 # ------------------------------------------------------------------
2169 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2170 #$bd = "${bd}T23:59:59";
2172 my $bills = $self->editor->search_money_billing(
2174 billing_ts => { '>=' => $bd },
2175 xact => $self->circ->id,
2176 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2180 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2182 for my $bill (@$bills) {
2183 unless( $U->is_true($bill->voided) ) {
2184 $logger->info("backdate voiding bill ".$bill->id);
2186 $bill->void_time('now');
2187 $bill->voider($self->editor->requestor->id);
2188 my $n = $bill->note || "";
2189 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2191 $self->bail_on_events($self->editor->event)
2192 unless $self->editor->update_money_billing($bill);
2200 sub find_patron_from_copy {
2202 my $circs = $self->editor->search_action_circulation(
2203 { target_copy => $self->copy->id, checkin_time => undef });
2204 my $circ = $circs->[0];
2205 return unless $circ;
2206 my $u = $self->editor->retrieve_actor_user($circ->usr)
2207 or return $self->bail_on_events($self->editor->event);
2211 sub check_checkin_copy_status {
2213 my $copy = $self->copy;
2219 my $status = $U->copy_status($copy->status)->id;
2222 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2223 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2224 $status == OILS_COPY_STATUS_IN_PROCESS ||
2225 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2226 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2227 $status == OILS_COPY_STATUS_CATALOGING ||
2228 $status == OILS_COPY_STATUS_RESHELVING );
2230 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2231 if( $status == OILS_COPY_STATUS_LOST );
2233 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2234 if( $status == OILS_COPY_STATUS_MISSING );
2236 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2241 # --------------------------------------------------------------------------
2242 # On checkin, we need to return as many relevant objects as we can
2243 # --------------------------------------------------------------------------
2244 sub checkin_flesh_events {
2247 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2248 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2249 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2253 for my $evt (@{$self->events}) {
2256 $payload->{copy} = $U->unflesh_copy($self->copy);
2257 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2258 $payload->{circ} = $self->circ;
2259 $payload->{transit} = $self->transit;
2260 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2262 # $self->hold may or may not have been replaced with a
2263 # valid hold after processing a cancelled hold
2264 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2266 $evt->{payload} = $payload;
2271 my( $self, $msg ) = @_;
2272 my $bc = ($self->copy) ? $self->copy->barcode :
2275 my $usr = ($self->patron) ? $self->patron->id : "";
2276 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2277 ", recipient=$usr, copy=$bc");
2283 $self->log_me("do_renew()");
2284 $self->is_renewal(1);
2286 # Make sure there is an open circ to renew that is not
2287 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2288 my $circ = $self->editor->search_action_circulation(
2289 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2292 $circ = $self->editor->search_action_circulation(
2294 target_copy => $self->copy->id,
2295 stop_fines => OILS_STOP_FINES_MAX_FINES,
2296 checkin_time => undef
2301 return $self->bail_on_events($self->editor->event) unless $circ;
2303 # A user is not allowed to renew another user's items without permission
2304 unless( $circ->usr eq $self->editor->requestor->id ) {
2305 return $self->bail_on_events($self->editor->events)
2306 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2309 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2310 if $circ->renewal_remaining < 1;
2312 # -----------------------------------------------------------------
2314 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2317 $self->run_renew_permit;
2320 $self->do_checkin();
2321 return if $self->bail_out;
2323 unless( $self->permit_override ) {
2325 return if $self->bail_out;
2326 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2327 $self->remove_event('ITEM_NOT_CATALOGED');
2330 $self->override_events;
2331 return if $self->bail_out;
2334 $self->do_checkout();
2339 my( $self, $evt ) = @_;
2340 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2341 $logger->debug("circulator: removing event from list: $evt");
2342 my @events = @{$self->events};
2343 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2348 my( $self, $evt ) = @_;
2349 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2350 return grep { $_->{textcode} eq $evt } @{$self->events};
2355 sub run_renew_permit {
2360 if(!$self->legacy_script_support) {
2361 my $results = $self->run_indb_circ_test;
2362 unless($self->circ_test_success) {
2363 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2368 my $runner = $self->script_runner;
2370 $runner->load($self->circ_permit_renew);
2371 my $result = $runner->run or
2372 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2373 my $events = $result->{events};
2376 $logger->activity("ciculator: circ_permit_renew for user ".
2377 $self->patron->id." returned events: @$events") if @$events;
2379 $self->push_events(OpenILS::Event->new($_)) for @$events;
2381 $logger->debug("circulator: re-creating script runner to be safe");
2382 #$self->mk_script_runner;