1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
8 use OpenILS::Application::AppUtils;
9 my $U = "OpenILS::Application::AppUtils";
13 my $legacy_script_support = 0;
18 my $conf = OpenSRF::Utils::SettingsClient->new;
19 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
20 my @pfx = ( @pfx2, "scripts" );
22 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
23 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
24 my $d = $conf->config_value( @pfx, 'circ_duration' );
25 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
26 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
27 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
28 my $lb = $conf->config_value( @pfx2, 'script_path' );
30 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
31 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
33 $logger->error( "Missing circ script(s)" )
34 unless( $p and $c and $d and $f and $m and $pr );
36 $scripts{circ_permit_patron} = $p;
37 $scripts{circ_permit_copy} = $c;
38 $scripts{circ_duration} = $d;
39 $scripts{circ_recurring_fines}= $f;
40 $scripts{circ_max_fines} = $m;
41 $scripts{circ_permit_renew} = $pr;
43 $lb = [ $lb ] unless ref($lb);
47 "circulator: Loaded rules scripts for circ: " .
48 "circ permit patron = $p, ".
49 "circ permit copy = $c, ".
50 "circ duration = $d, ".
51 "circ recurring fines = $f, " .
52 "circ max fines = $m, ".
53 "circ renew permit = $pr. ".
55 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
60 __PACKAGE__->register_method(
61 method => "run_method",
62 api_name => "open-ils.circ.checkout.permit",
64 Determines if the given checkout can occur
65 @param authtoken The login session key
66 @param params A trailing hash of named params including
67 barcode : The copy barcode,
68 patron : The patron the checkout is occurring for,
69 renew : true or false - whether or not this is a renewal
70 @return The event that occurred during the permit check.
74 __PACKAGE__->register_method (
75 method => 'run_method',
76 api_name => 'open-ils.circ.checkout.permit.override',
77 signature => q/@see open-ils.circ.checkout.permit/,
81 __PACKAGE__->register_method(
82 method => "run_method",
83 api_name => "open-ils.circ.checkout",
86 @param authtoken The login session key
87 @param params A named hash of params including:
89 barcode If no copy is provided, the copy is retrieved via barcode
90 copyid If no copy or barcode is provide, the copy id will be use
91 patron The patron's id
92 noncat True if this is a circulation for a non-cataloted item
93 noncat_type The non-cataloged type id
94 noncat_circ_lib The location for the noncat circ.
95 precat The item has yet to be cataloged
96 dummy_title The temporary title of the pre-cataloded item
97 dummy_author The temporary authr of the pre-cataloded item
98 Default is the home org of the staff member
99 @return The SUCCESS event on success, any other event depending on the error
102 __PACKAGE__->register_method(
103 method => "run_method",
104 api_name => "open-ils.circ.checkin",
107 Generic super-method for handling all copies
108 @param authtoken The login session key
109 @param params Hash of named parameters including:
110 barcode - The copy barcode
111 force - If true, copies in bad statuses will be checked in and give good statuses
116 __PACKAGE__->register_method(
117 method => "run_method",
118 api_name => "open-ils.circ.checkin.override",
119 signature => q/@see open-ils.circ.checkin/
122 __PACKAGE__->register_method(
123 method => "run_method",
124 api_name => "open-ils.circ.renew.override",
125 signature => q/@see open-ils.circ.renew/,
129 __PACKAGE__->register_method(
130 method => "run_method",
131 api_name => "open-ils.circ.renew",
132 notes => <<" NOTES");
133 PARAMS( authtoken, circ => circ_id );
134 open-ils.circ.renew(login_session, circ_object);
135 Renews the provided circulation. login_session is the requestor of the
136 renewal and if the logged in user is not the same as circ->usr, then
137 the logged in user must have RENEW_CIRC permissions.
140 __PACKAGE__->register_method(
141 method => "run_method",
142 api_name => "open-ils.circ.checkout.full");
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkout.full.override");
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.inspect",
151 Returns the circ matrix test result and, on success, the rule set and matrix test object
158 my( $self, $conn, $auth, $args ) = @_;
159 translate_legacy_args($args);
160 my $api = $self->api_name;
163 OpenILS::Application::Circ::Circulator->new($auth, %$args);
165 return circ_events($circulator) if $circulator->bail_out;
167 # --------------------------------------------------------------------------
168 # Go ahead and load the script runner to make sure we have all
169 # of the objects we need
170 # --------------------------------------------------------------------------
171 $circulator->is_renewal(1) if $api =~ /renew/;
172 $circulator->is_checkin(1) if $api =~ /checkin/;
173 $circulator->check_penalty_on_renew(1) if
174 $circulator->is_renewal and $U->ou_ancestor_setting_value(
175 $circulator->editor->requestor->ws_ou, 'circ.renew.check_penalty', $circulator->editor);
177 if($legacy_script_support and not $circulator->is_checkin) {
178 $circulator->mk_script_runner();
179 $circulator->legacy_script_support(1);
180 $circulator->circ_permit_patron($scripts{circ_permit_patron});
181 $circulator->circ_permit_copy($scripts{circ_permit_copy});
182 $circulator->circ_duration($scripts{circ_duration});
183 $circulator->circ_permit_renew($scripts{circ_permit_renew});
185 $circulator->mk_env();
187 return circ_events($circulator) if $circulator->bail_out;
190 $circulator->override(1) if $api =~ /override/o;
192 if( $api =~ /checkout\.permit/ ) {
193 $circulator->do_permit();
195 } elsif( $api =~ /checkout.full/ ) {
197 $circulator->do_permit();
198 unless( $circulator->bail_out ) {
199 $circulator->events([]);
200 $circulator->do_checkout();
203 } elsif( $api =~ /inspect/ ) {
204 my $data = $circulator->do_inspect();
205 $circulator->editor->rollback;
208 } elsif( $api =~ /checkout/ ) {
209 $circulator->do_checkout();
211 } elsif( $api =~ /checkin/ ) {
212 $circulator->do_checkin();
214 } elsif( $api =~ /renew/ ) {
215 $circulator->is_renewal(1);
216 $circulator->do_renew();
219 if( $circulator->bail_out ) {
222 # make sure no success event accidentally slip in
224 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
227 my @e = @{$circulator->events};
228 push( @ee, $_->{textcode} ) for @e;
229 $logger->info("circulator: bailing out with events: @ee");
231 $circulator->editor->rollback;
234 $circulator->editor->commit;
237 $circulator->script_runner->cleanup if $circulator->script_runner;
239 $conn->respond_complete(circ_events($circulator));
241 unless($circulator->bail_out) {
242 $circulator->do_hold_notify($circulator->notify_hold)
243 if $circulator->notify_hold;
244 $circulator->retarget_holds if $circulator->retarget;
250 my @e = @{$circ->events};
251 # if we have multiple events, SUCCESS should not be one of them;
252 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
253 return (@e == 1) ? $e[0] : \@e;
257 sub translate_legacy_args {
260 if( $$args{barcode} ) {
261 $$args{copy_barcode} = $$args{barcode};
262 delete $$args{barcode};
265 if( $$args{copyid} ) {
266 $$args{copy_id} = $$args{copyid};
267 delete $$args{copyid};
270 if( $$args{patronid} ) {
271 $$args{patron_id} = $$args{patronid};
272 delete $$args{patronid};
275 if( $$args{patron} and !ref($$args{patron}) ) {
276 $$args{patron_id} = $$args{patron};
277 delete $$args{patron};
281 if( $$args{noncat} ) {
282 $$args{is_noncat} = $$args{noncat};
283 delete $$args{noncat};
286 if( $$args{precat} ) {
287 $$args{is_precat} = $$args{precat};
288 delete $$args{precat};
294 # --------------------------------------------------------------------------
295 # This package actually manages all of the circulation logic
296 # --------------------------------------------------------------------------
297 package OpenILS::Application::Circ::Circulator;
298 use strict; use warnings;
299 use vars q/$AUTOLOAD/;
301 use OpenILS::Utils::Fieldmapper;
302 use OpenSRF::Utils::Cache;
303 use Digest::MD5 qw(md5_hex);
304 use DateTime::Format::ISO8601;
305 use OpenILS::Utils::PermitHold;
306 use OpenSRF::Utils qw/:datetime/;
307 use OpenSRF::Utils::SettingsClient;
308 use OpenILS::Application::Circ::Holds;
309 use OpenILS::Application::Circ::Transit;
310 use OpenSRF::Utils::Logger qw(:logger);
311 use OpenILS::Utils::CStoreEditor qw/:funcs/;
312 use OpenILS::Application::Circ::ScriptBuilder;
313 use OpenILS::Const qw/:const/;
315 my $holdcode = "OpenILS::Application::Circ::Holds";
316 my $transcode = "OpenILS::Application::Circ::Transit";
321 # --------------------------------------------------------------------------
322 # Add a pile of automagic getter/setter methods
323 # --------------------------------------------------------------------------
324 my @AUTOLOAD_FIELDS = qw/
339 check_penalty_on_renew
366 recurring_fines_level
379 cancelled_hold_transit
388 legacy_script_support
398 my $type = ref($self) or die "$self is not an object";
400 my $name = $AUTOLOAD;
403 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
404 $logger->error("circulator: $type: invalid autoload field: $name");
405 die "$type: invalid autoload field: $name\n"
410 *{"${type}::${name}"} = sub {
413 $s->{$name} = $v if defined $v;
417 return $self->$name($data);
422 my( $class, $auth, %args ) = @_;
423 $class = ref($class) || $class;
424 my $self = bless( {}, $class );
428 new_editor(xact => 1, authtoken => $auth) );
430 unless( $self->editor->checkauth ) {
431 $self->bail_on_events($self->editor->event);
435 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
437 $self->$_($args{$_}) for keys %args;
440 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
442 # if this is a renewal, default to desk_renewal
443 $self->desk_renewal(1) unless
444 $self->opac_renewal or $self->phone_renewal;
450 # --------------------------------------------------------------------------
451 # True if we should discontinue processing
452 # --------------------------------------------------------------------------
454 my( $self, $bool ) = @_;
455 if( defined $bool ) {
456 $logger->info("circulator: BAILING OUT") if $bool;
457 $self->{bail_out} = $bool;
459 return $self->{bail_out};
464 my( $self, @evts ) = @_;
467 $logger->info("circulator: pushing event ".$e->{textcode});
468 push( @{$self->events}, $e ) unless
469 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
475 my $key = md5_hex( time() . rand() . "$$" );
476 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
477 return $self->permit_key($key);
480 sub check_permit_key {
482 my $key = $self->permit_key;
483 return 0 unless $key;
484 my $k = "oils_permit_key_$key";
485 my $one = $self->cache_handle->get_cache($k);
486 $self->cache_handle->delete_cache($k);
487 return ($one) ? 1 : 0;
492 my $e = $self->editor;
494 # --------------------------------------------------------------------------
495 # Grab the fleshed copy
496 # --------------------------------------------------------------------------
497 unless($self->is_noncat) {
501 flesh_fields => {acp => ['call_number'], acn => ['record']}
504 $copy = $e->retrieve_asset_copy(
505 [$self->copy_id, $flesh ]) or return $e->event;
507 } elsif( $self->copy_barcode ) {
509 $copy = $e->search_asset_copy(
510 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
515 $self->volume($copy->call_number);
516 $self->title($self->volume->record);
517 $self->copy->call_number($self->volume->id);
518 $self->volume->record($self->title->id);
519 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
520 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
521 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
522 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
525 # We can't renew if there is no copy
526 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
527 if $self->is_renewal;
532 return undef if $self->is_checkin;
534 # --------------------------------------------------------------------------
536 # --------------------------------------------------------------------------
538 if( $self->patron_id ) {
539 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
541 } elsif( $self->patron_barcode ) {
543 my $card = $e->search_actor_card(
544 {barcode => $self->patron_barcode})->[0] or return $e->event;
546 $patron = $e->search_actor_user(
547 {card => $card->id})->[0] or return $e->event;
550 if( my $copy = $self->copy ) {
551 my $circs = $e->search_action_circulation(
552 {target_copy => $copy->id, checkin_time => undef});
554 if( my $circ = $circs->[0] ) {
555 $patron = $e->retrieve_actor_user($circ->usr)
561 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
562 unless $self->patron($patron);
565 # --------------------------------------------------------------------------
566 # This builds the script runner environment and fetches most of the
568 # --------------------------------------------------------------------------
569 sub mk_script_runner {
575 qw/copy copy_barcode copy_id patron
576 patron_id patron_barcode volume title editor/;
578 # Translate our objects into the ScriptBuilder args hash
579 $$args{$_} = $self->$_() for @fields;
581 $args->{ignore_user_status} = 1 if $self->is_checkin;
582 $$args{fetch_patron_by_circ_copy} = 1;
583 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
585 if( my $pco = $self->pending_checkouts ) {
586 $logger->info("circulator: we were given a pending checkouts number of $pco");
587 $$args{patronItemsOut} = $pco;
590 # This fetches most of the objects we need
591 $self->script_runner(
592 OpenILS::Application::Circ::ScriptBuilder->build($args));
594 # Now we translate the ScriptBuilder objects back into self
595 $self->$_($$args{$_}) for @fields;
597 my @evts = @{$args->{_events}} if $args->{_events};
599 $logger->debug("circulator: script builder returned events: @evts") if @evts;
603 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
604 if(!$self->is_noncat and
606 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
610 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
611 return $self->bail_on_events(@e);
616 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
617 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
618 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
619 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
623 # We can't renew if there is no copy
624 return $self->bail_on_events(@evts) if
625 $self->is_renewal and !$self->copy;
627 # Set some circ-specific flags in the script environment
628 my $evt = "environment";
629 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
631 if( $self->is_noncat ) {
632 $self->script_runner->insert("$evt.isNonCat", 1);
633 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
636 if( $self->is_precat ) {
637 $self->script_runner->insert("environment.isPrecat", 1, 1);
640 $self->script_runner->add_path( $_ ) for @$script_libs;
645 # --------------------------------------------------------------------------
646 # Does the circ permit work
647 # --------------------------------------------------------------------------
651 $self->log_me("do_permit()");
653 unless( $self->editor->requestor->id == $self->patron->id ) {
654 return $self->bail_on_events($self->editor->event)
655 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
658 $self->check_captured_holds();
659 $self->do_copy_checks();
660 return if $self->bail_out;
661 $self->run_patron_permit_scripts();
662 $self->run_copy_permit_scripts()
663 unless $self->is_precat or $self->is_noncat;
664 $self->check_item_deposit_events();
665 $self->override_events() unless
666 $self->is_renewal and not $self->check_penalty_on_renew;
667 return if $self->bail_out;
669 if( $self->is_precat ) {
672 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
673 return $self->bail_out(1) unless $self->is_renewal;
679 payload => $self->mk_permit_key));
682 sub check_item_deposit_events {
684 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED')) if $self->is_deposit;
685 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED')) if $self->is_rental;
688 sub check_captured_holds {
690 my $copy = $self->copy;
691 my $patron = $self->patron;
693 return undef unless $copy;
695 my $s = $U->copy_status($copy->status)->id;
696 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
697 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
699 # Item is on the holds shelf, make sure it's going to the right person
700 my $holds = $self->editor->search_action_hold_request(
703 current_copy => $copy->id ,
704 capture_time => { '!=' => undef },
705 cancel_time => undef,
706 fulfillment_time => undef
712 if( $holds and $$holds[0] ) {
713 return undef if $$holds[0]->usr == $patron->id;
716 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
718 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
724 my $copy = $self->copy;
727 my $stat = $U->copy_status($copy->status)->id;
729 # We cannot check out a copy if it is in-transit
730 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
731 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
734 $self->handle_claims_returned();
735 return if $self->bail_out;
737 # no claims returned circ was found, check if there is any open circ
738 unless( $self->is_renewal ) {
739 my $circs = $self->editor->search_action_circulation(
740 { target_copy => $copy->id, checkin_time => undef }
743 return $self->bail_on_events(
744 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
749 sub send_penalty_request {
751 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
752 $self->penalty_request(
754 'open-ils.penalty.patron_penalty.calculate',
756 authtoken => $self->editor->authtoken,
757 patron => $self->patron } ) );
760 sub gather_penalty_request {
762 return [] unless $self->penalty_request;
763 my $data = $self->penalty_request->recv;
765 throw $data if UNIVERSAL::isa($data,'Error');
766 $data = $data->content;
767 return $data->{fatal_penalties};
769 $logger->error("circulator: penalty request returned no data");
773 my $LEGACY_CIRC_EVENT_MAP = {
774 'actor.usr.barred' => 'PATRON_BARRED',
775 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
776 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
777 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
778 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
779 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
780 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
781 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
782 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
786 # ---------------------------------------------------------------------
787 # This pushes any patron-related events into the list but does not
788 # set bail_out for any events
789 # ---------------------------------------------------------------------
790 sub run_patron_permit_scripts {
792 my $runner = $self->script_runner;
793 my $patronid = $self->patron->id;
797 if(!$self->legacy_script_support) {
799 my $results = $self->run_indb_circ_test;
800 unless($self->circ_test_success) {
801 push(@allevents, OpenILS::Event->new(
802 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
807 $self->send_penalty_request() unless
808 $self->is_renewal and not $self->check_penalty_on_renew;
811 # --------------------------------------------------------------------- # Now run the patron permit script
812 # ---------------------------------------------------------------------
813 $runner->load($self->circ_permit_patron);
814 my $result = $runner->run or
815 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
817 my $patron_events = $result->{events};
819 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
820 [] : $self->gather_penalty_request();
822 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
825 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
827 $self->push_events(@allevents);
830 sub run_indb_circ_test {
832 return $self->matrix_test_result if $self->matrix_test_result;
834 my $dbfunc = ($self->is_renewal) ?
835 'action.item_user_renew_test' : 'action.item_user_circ_test';
837 my $results = ($self->matrix_test_result) ?
838 $self->matrix_test_result :
839 $self->editor->json_query(
842 $self->editor->requestor->ws_ou,
843 ($self->is_precat) ? undef : $self->copy->id,
849 $self->circ_test_success($U->is_true($results->[0]->{success}));
850 if($self->circ_test_success) {
851 $self->circ_matrix_test(
852 $self->editor->retrieve_config_circ_matrix_test(
853 $results->[0]->{matchpoint}
858 if($self->circ_test_success) {
859 $self->circ_matrix_ruleset(
860 $self->editor->retrieve_config_circ_matrix_ruleset([
861 $results->[0]->{matchpoint},
864 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
872 return $self->matrix_test_result($results);
877 $self->run_indb_circ_test;
880 circ_test_success => $self->circ_test_success,
881 failure_events => [],
885 unless($self->circ_test_success) {
886 push(@{$results->{failure_codes}},
887 $_->{fail_part}) for @{$self->matrix_test_result};
888 push(@{$results->{failure_events}},
889 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
890 for @{$self->matrix_test_result};
894 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
895 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
896 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
898 my $policy = $self->get_circ_policy(
899 $duration_rule, $recurring_fine_rule, $max_fine_rule);
901 $$results{$_} = $$policy{$_} for keys %$policy;
905 # ---------------------------------------------------------------------
906 # Loads the circ policy info for duration, recurring fine, and max
907 # fine based on the current copy
908 # ---------------------------------------------------------------------
909 sub get_circ_policy {
910 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
913 duration_rule => $duration_rule->name,
914 recurring_fine_rule => $recurring_fine_rule->name,
915 max_fine_rule => $max_fine_rule->name,
916 max_fine => $self->get_max_fine_amount($max_fine_rule),
917 fine_interval => $recurring_fine_rule->recurance_interval,
918 renewal_remaining => $duration_rule->max_renewals
921 $policy->{duration} = $duration_rule->shrt
922 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
923 $policy->{duration} = $duration_rule->normal
924 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
925 $policy->{duration} = $duration_rule->extended
926 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
928 $policy->{recurring_fine} = $recurring_fine_rule->low
929 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
930 $policy->{recurring_fine} = $recurring_fine_rule->normal
931 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
932 $policy->{recurring_fine} = $recurring_fine_rule->high
933 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
938 sub get_max_fine_amount {
940 my $max_fine_rule = shift;
941 my $max_amount = $max_fine_rule->amount;
943 # if is_percent is true then the max->amount is
944 # use as a percentage of the copy price
945 if ($U->is_true($max_fine_rule->is_percent)) {
947 my $ol = ($self->is_precat) ?
948 $self->editor->requestor->ws_ou : $self->volume->owning_lib;
950 my $default_price = $U->ou_ancestor_setting_value(
951 $ol, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
952 my $charge_on_0 = $U->ou_ancestor_setting_value(
953 $ol, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
955 # Find the most appropriate "price" -- same definition as the
956 # LOST price. See OpenILS::Circ::new_set_circ_lost
957 $max_amount = $self->copy->price;
958 $max_amount = $default_price unless defined $max_amount;
959 $max_amount = 0 if $max_amount < 0;
960 $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
962 $max_amount *= $max_fine_rule->amount / 100;
970 sub run_copy_permit_scripts {
972 my $copy = $self->copy || return;
973 my $runner = $self->script_runner;
977 if(!$self->legacy_script_support) {
978 my $results = $self->run_indb_circ_test;
979 unless($self->circ_test_success) {
980 push(@allevents, OpenILS::Event->new(
981 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
985 # ---------------------------------------------------------------------
986 # Capture all of the copy permit events
987 # ---------------------------------------------------------------------
988 $runner->load($self->circ_permit_copy);
989 my $result = $runner->run or
990 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
991 my $copy_events = $result->{events};
993 # ---------------------------------------------------------------------
994 # Now collect all of the events together
995 # ---------------------------------------------------------------------
996 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
999 # See if this copy has an alert message
1000 my $ae = $self->check_copy_alert();
1001 push( @allevents, $ae ) if $ae;
1003 # uniquify the events
1004 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1005 @allevents = values %hash;
1008 $_->{payload} = $copy if
1009 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1012 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1014 $self->push_events(@allevents);
1018 sub check_copy_alert {
1020 return undef if $self->is_renewal;
1021 return OpenILS::Event->new(
1022 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1023 if $self->copy and $self->copy->alert_message;
1029 # --------------------------------------------------------------------------
1030 # If the call is overriding and has permissions to override every collected
1031 # event, the are cleared. Any event that the caller does not have
1032 # permission to override, will be left in the event list and bail_out will
1034 # XXX We need code in here to cancel any holds/transits on copies
1035 # that are being force-checked out
1036 # --------------------------------------------------------------------------
1037 sub override_events {
1039 my @events = @{$self->events};
1040 return unless @events;
1042 if(!$self->override) {
1043 return $self->bail_out(1)
1044 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1049 for my $e (@events) {
1050 my $tc = $e->{textcode};
1051 next if $tc eq 'SUCCESS';
1052 my $ov = "$tc.override";
1053 $logger->info("circulator: attempting to override event: $ov");
1055 return $self->bail_on_events($self->editor->event)
1056 unless( $self->editor->allowed($ov) );
1061 # --------------------------------------------------------------------------
1062 # If there is an open claimsreturn circ on the requested copy, close the
1063 # circ if overriding, otherwise bail out
1064 # --------------------------------------------------------------------------
1065 sub handle_claims_returned {
1067 my $copy = $self->copy;
1069 my $CR = $self->editor->search_action_circulation(
1071 target_copy => $copy->id,
1072 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1073 checkin_time => undef,
1077 return unless ($CR = $CR->[0]);
1081 # - If the caller has set the override flag, we will check the item in
1082 if($self->override) {
1084 $CR->checkin_time('now');
1085 $CR->checkin_lib($self->editor->requestor->ws_ou);
1086 $CR->checkin_staff($self->editor->requestor->id);
1088 $evt = $self->editor->event
1089 unless $self->editor->update_action_circulation($CR);
1092 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1095 $self->bail_on_events($evt) if $evt;
1100 # --------------------------------------------------------------------------
1101 # This performs the checkout
1102 # --------------------------------------------------------------------------
1106 $self->log_me("do_checkout()");
1108 # make sure perms are good if this isn't a renewal
1109 unless( $self->is_renewal ) {
1110 return $self->bail_on_events($self->editor->event)
1111 unless( $self->editor->allowed('COPY_CHECKOUT') );
1114 # verify the permit key
1115 unless( $self->check_permit_key ) {
1116 if( $self->permit_override ) {
1117 return $self->bail_on_events($self->editor->event)
1118 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1120 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1124 # if this is a non-cataloged circ, build the circ and finish
1125 if( $self->is_noncat ) {
1126 $self->checkout_noncat;
1128 OpenILS::Event->new('SUCCESS',
1129 payload => { noncat_circ => $self->circ }));
1133 if( $self->is_precat ) {
1134 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1135 $self->make_precat_copy;
1136 return if $self->bail_out;
1138 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1139 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1142 $self->do_copy_checks;
1143 return if $self->bail_out;
1145 $self->run_checkout_scripts();
1146 return if $self->bail_out;
1148 $self->build_checkout_circ_object();
1149 return if $self->bail_out;
1151 $self->apply_modified_due_date();
1152 return if $self->bail_out;
1154 return $self->bail_on_events($self->editor->event)
1155 unless $self->editor->create_action_circulation($self->circ);
1157 # refresh the circ to force local time zone for now
1158 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1160 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1162 return if $self->bail_out;
1164 $self->apply_deposit_fee();
1165 return if $self->bail_out;
1167 $self->handle_checkout_holds();
1168 return if $self->bail_out;
1170 # ------------------------------------------------------------------------------
1171 # Update the patron penalty info in the DB. Run it for permit-overrides or
1172 # renewals since both of those cases do not require the penalty server to
1173 # run during the permit phase of the checkout
1174 # ------------------------------------------------------------------------------
1175 if( $self->permit_override or $self->is_renewal ) {
1176 $U->update_patron_penalties(
1177 authtoken => $self->editor->authtoken,
1178 patron => $self->patron,
1183 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1185 OpenILS::Event->new('SUCCESS',
1187 copy => $U->unflesh_copy($self->copy),
1188 circ => $self->circ,
1190 holds_fulfilled => $self->fulfilled_holds,
1191 deposit_billing => $self->deposit_billing,
1192 rental_billing => $self->rental_billing
1198 sub apply_deposit_fee {
1200 my $copy = $self->copy;
1201 return unless $self->is_deposit or $self->is_rental;
1203 my $bill = Fieldmapper::money::billing->new;
1204 my $amount = $copy->deposit_amount;
1207 if($self->is_deposit) {
1208 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1209 $self->deposit_billing($bill);
1211 $billing_type = OILS_BILLING_TYPE_RENTAL;
1212 $self->rental_billing($bill);
1215 $bill->xact($self->circ->id);
1216 $bill->amount($amount);
1217 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1218 $bill->billing_type($billing_type);
1219 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1221 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1226 my $copy = $self->copy;
1228 my $stat = $copy->status if ref $copy->status;
1229 my $loc = $copy->location if ref $copy->location;
1230 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1232 $copy->status($stat->id) if $stat;
1233 $copy->location($loc->id) if $loc;
1234 $copy->circ_lib($circ_lib->id) if $circ_lib;
1235 $copy->editor($self->editor->requestor->id);
1236 $copy->edit_date('now');
1237 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1239 return $self->bail_on_events($self->editor->event)
1240 unless $self->editor->update_asset_copy($self->copy);
1242 $copy->status($U->copy_status($copy->status));
1243 $copy->location($loc) if $loc;
1244 $copy->circ_lib($circ_lib) if $circ_lib;
1248 sub bail_on_events {
1249 my( $self, @evts ) = @_;
1250 $self->push_events(@evts);
1254 sub handle_checkout_holds {
1257 my $copy = $self->copy;
1258 my $patron = $self->patron;
1260 my $holds = $self->editor->search_action_hold_request(
1262 current_copy => $copy->id ,
1263 cancel_time => undef,
1264 fulfillment_time => undef
1270 # XXX We should only fulfill one hold here...
1271 # XXX If a hold was transited to the user who is checking out
1272 # the item, we need to make sure that hold is what's grabbed
1275 # for now, just sort by id to get what should be the oldest hold
1276 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1277 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1278 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1281 my $hold = $myholds[0];
1283 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1285 # if the hold was never officially captured, capture it.
1286 $hold->capture_time('now') unless $hold->capture_time;
1288 # just make sure it's set correctly
1289 $hold->current_copy($copy->id);
1291 $hold->fulfillment_time('now');
1292 $hold->fulfillment_staff($self->editor->requestor->id);
1293 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1295 return $self->bail_on_events($self->editor->event)
1296 unless $self->editor->update_action_hold_request($hold);
1298 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1300 push( @fulfilled, $hold->id );
1303 # If there are any holds placed for other users that point to this copy,
1304 # then we need to un-target those holds so the targeter can pick a new copy
1307 $logger->info("circulator: un-targeting hold ".$_->id.
1308 " because copy ".$copy->id." is getting checked out");
1310 # - make the targeter process this hold at next run
1311 $_->clear_prev_check_time;
1313 # - clear out the targetted copy
1314 $_->clear_current_copy;
1315 $_->clear_capture_time;
1317 return $self->bail_on_event($self->editor->event)
1318 unless $self->editor->update_action_hold_request($_);
1322 $self->fulfilled_holds(\@fulfilled);
1327 sub run_checkout_scripts {
1331 my $runner = $self->script_runner;
1340 if(!$self->legacy_script_support) {
1341 $self->run_indb_circ_test();
1342 $duration = $self->circ_matrix_ruleset->duration_rule;
1343 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1344 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1348 $runner->load($self->circ_duration);
1350 my $result = $runner->run or
1351 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1353 $duration_name = $result->{durationRule};
1354 $recurring_name = $result->{recurringFinesRule};
1355 $max_fine_name = $result->{maxFine};
1358 $duration_name = $duration->name if $duration;
1359 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1362 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1363 return $self->bail_on_events($evt) if $evt;
1365 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1366 return $self->bail_on_events($evt) if $evt;
1368 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1369 return $self->bail_on_events($evt) if $evt;
1374 # The item circulates with an unlimited duration
1380 $self->duration_rule($duration);
1381 $self->recurring_fines_rule($recurring);
1382 $self->max_fine_rule($max_fine);
1386 sub build_checkout_circ_object {
1389 my $circ = Fieldmapper::action::circulation->new;
1390 my $duration = $self->duration_rule;
1391 my $max = $self->max_fine_rule;
1392 my $recurring = $self->recurring_fines_rule;
1393 my $copy = $self->copy;
1394 my $patron = $self->patron;
1398 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1400 my $dname = $duration->name;
1401 my $mname = $max->name;
1402 my $rname = $recurring->name;
1404 $logger->debug("circulator: building circulation ".
1405 "with duration=$dname, maxfine=$mname, recurring=$rname");
1407 $circ->duration($policy->{duration});
1408 $circ->recuring_fine($policy->{recurring_fine});
1409 $circ->duration_rule($duration->name);
1410 $circ->recuring_fine_rule($recurring->name);
1411 $circ->max_fine_rule($max->name);
1412 $circ->max_fine($policy->{max_fine});
1413 $circ->fine_interval($recurring->recurance_interval);
1414 $circ->renewal_remaining($duration->max_renewals);
1418 $logger->info("circulator: copy found with an unlimited circ duration");
1419 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1420 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1421 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1422 $circ->renewal_remaining(0);
1425 $circ->target_copy( $copy->id );
1426 $circ->usr( $patron->id );
1427 $circ->circ_lib( $self->circ_lib );
1429 if( $self->is_renewal ) {
1430 $circ->opac_renewal('t') if $self->opac_renewal;
1431 $circ->phone_renewal('t') if $self->phone_renewal;
1432 $circ->desk_renewal('t') if $self->desk_renewal;
1433 $circ->renewal_remaining($self->renewal_remaining);
1434 $circ->circ_staff($self->editor->requestor->id);
1438 # if the user provided an overiding checkout time,
1439 # (e.g. the checkout really happened several hours ago), then
1440 # we apply that here. Does this need a perm??
1441 $circ->xact_start(clense_ISO8601($self->checkout_time))
1442 if $self->checkout_time;
1444 # if a patron is renewing, 'requestor' will be the patron
1445 $circ->circ_staff($self->editor->requestor->id);
1446 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1452 sub apply_modified_due_date {
1454 my $circ = $self->circ;
1455 my $copy = $self->copy;
1457 if( $self->due_date ) {
1459 return $self->bail_on_events($self->editor->event)
1460 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1462 $circ->due_date(clense_ISO8601($self->due_date));
1466 # if the due_date lands on a day when the location is closed
1467 return unless $copy and $circ->due_date;
1469 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1471 # due-date overlap should be determined by the location the item
1472 # is checked out from, not the owning or circ lib of the item
1473 my $org = $self->editor->requestor->ws_ou;
1475 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1476 " with an item due date of ".$circ->due_date );
1478 my $dateinfo = $U->storagereq(
1479 'open-ils.storage.actor.org_unit.closed_date.overlap',
1480 $org, $circ->due_date );
1483 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1484 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1486 # XXX make the behavior more dynamic
1487 # for now, we just push the due date to after the close date
1488 $circ->due_date($dateinfo->{end});
1495 sub create_due_date {
1496 my( $self, $duration ) = @_;
1497 # if there is a raw time component (e.g. from postgres),
1498 # turn it into an interval that interval_to_seconds can parse
1499 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1500 my ($sec,$min,$hour,$mday,$mon,$year) =
1501 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1502 $year += 1900; $mon += 1;
1503 my $due_date = sprintf(
1504 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1505 $year, $mon, $mday, $hour, $min, $sec);
1511 sub make_precat_copy {
1513 my $copy = $self->copy;
1516 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1518 $copy->editor($self->editor->requestor->id);
1519 $copy->edit_date('now');
1520 $copy->dummy_title($self->dummy_title);
1521 $copy->dummy_author($self->dummy_author);
1523 $self->update_copy();
1527 $logger->info("circulator: Creating a new precataloged ".
1528 "copy in checkout with barcode " . $self->copy_barcode);
1530 $copy = Fieldmapper::asset::copy->new;
1531 $copy->circ_lib($self->circ_lib);
1532 $copy->creator($self->editor->requestor->id);
1533 $copy->editor($self->editor->requestor->id);
1534 $copy->barcode($self->copy_barcode);
1535 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1536 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1537 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1539 $copy->dummy_title($self->dummy_title || "");
1540 $copy->dummy_author($self->dummy_author || "");
1542 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1544 $self->push_events($self->editor->event);
1548 # this is a little bit of a hack, but we need to
1549 # get the copy into the script runner
1550 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1554 sub checkout_noncat {
1560 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1561 my $count = $self->noncat_count || 1;
1562 my $cotime = clense_ISO8601($self->checkout_time) || "";
1564 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1568 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1569 $self->editor->requestor->id,
1577 $self->push_events($evt);
1588 $self->log_me("do_checkin()");
1591 return $self->bail_on_events(
1592 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1595 if( $self->checkin_check_holds_shelf() ) {
1596 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1597 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1598 $self->checkin_flesh_events;
1602 unless( $self->is_renewal ) {
1603 return $self->bail_on_events($self->editor->event)
1604 unless $self->editor->allowed('COPY_CHECKIN');
1607 $self->push_events($self->check_copy_alert());
1608 $self->push_events($self->check_checkin_copy_status());
1610 # the renew code will have already found our circulation object
1611 unless( $self->is_renewal and $self->circ ) {
1612 my $circs = $self->editor->search_action_circulation(
1613 { target_copy => $self->copy->id, checkin_time => undef });
1614 $self->circ($$circs[0]);
1616 # for now, just warn if there are multiple open circs on a copy
1617 $logger->warn("circulator: we have ".scalar(@$circs).
1618 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1621 # if the circ is marked as 'claims returned', add the event to the list
1622 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1623 if ($self->circ and $self->circ->stop_fines
1624 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1626 $self->check_circ_deposit();
1628 # handle the overridable events
1629 $self->override_events unless $self->is_renewal;
1630 return if $self->bail_out;
1634 $self->editor->search_action_transit_copy(
1635 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1639 $self->checkin_handle_circ;
1640 return if $self->bail_out;
1641 $self->checkin_changed(1);
1643 } elsif( $self->transit ) {
1644 my $hold_transit = $self->process_received_transit;
1645 $self->checkin_changed(1);
1647 if( $self->bail_out ) {
1648 $self->checkin_flesh_events;
1652 if( my $e = $self->check_checkin_copy_status() ) {
1653 # If the original copy status is special, alert the caller
1654 my $ev = $self->events;
1655 $self->events([$e]);
1656 $self->override_events;
1657 return if $self->bail_out;
1661 if( $hold_transit or
1662 $U->copy_status($self->copy->status)->id
1663 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1666 if( $hold_transit ) {
1667 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1669 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1674 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1676 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1677 $self->reshelve_copy(1);
1678 $self->cancelled_hold_transit(1);
1679 $self->notify_hold(0); # don't notify for cancelled holds
1680 return if $self->bail_out;
1684 # hold transited to correct location
1685 $self->checkin_flesh_events;
1690 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1692 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1693 " that is in-transit, but there is no transit.. repairing");
1694 $self->reshelve_copy(1);
1695 return if $self->bail_out;
1698 if( $self->is_renewal ) {
1699 $self->push_events(OpenILS::Event->new('SUCCESS'));
1703 # ------------------------------------------------------------------------------
1704 # Circulations and transits are now closed where necessary. Now go on to see if
1705 # this copy can fulfill a hold or needs to be routed to a different location
1706 # ------------------------------------------------------------------------------
1708 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1709 return if $self->bail_out;
1711 } else { # not needed for a hold
1713 my $circ_lib = (ref $self->copy->circ_lib) ?
1714 $self->copy->circ_lib->id : $self->copy->circ_lib;
1716 if( $self->remote_hold ) {
1717 $circ_lib = $self->remote_hold->pickup_lib;
1718 $logger->warn("circulator: Copy ".$self->copy->barcode.
1719 " is on a remote hold's shelf, sending to $circ_lib");
1722 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1724 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1726 $self->checkin_handle_precat();
1727 return if $self->bail_out;
1731 my $bc = $self->copy->barcode;
1732 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1733 $self->checkin_build_copy_transit($circ_lib);
1734 return if $self->bail_out;
1735 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1739 $self->reshelve_copy;
1740 return if $self->bail_out;
1742 unless($self->checkin_changed) {
1744 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1745 my $stat = $U->copy_status($self->copy->status)->id;
1747 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1748 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1749 $self->bail_out(1); # no need to commit anything
1753 $self->push_events(OpenILS::Event->new('SUCCESS'))
1754 unless @{$self->events};
1758 # ------------------------------------------------------------------------------
1759 # Update the patron penalty info in the DB
1760 # ------------------------------------------------------------------------------
1761 $U->update_patron_penalties(
1762 authtoken => $self->editor->authtoken,
1763 patron => $self->patron,
1764 background => 1 ) if $self->is_checkin;
1766 $self->checkin_flesh_events;
1770 # if a deposit was payed for this item, push the event
1771 sub check_circ_deposit {
1773 return unless $self->circ;
1774 my $deposit = $self->editor->search_money_billing(
1775 { billing_type => OILS_BILLING_TYPE_DEPOSIT,
1776 xact => $self->circ->id,
1778 }, {idlist => 1})->[0];
1780 $self->push_events(OpenILS::Event->new(
1781 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1786 my $force = $self->force || shift;
1787 my $copy = $self->copy;
1789 my $stat = $U->copy_status($copy->status)->id;
1792 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1793 $stat != OILS_COPY_STATUS_CATALOGING and
1794 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1795 $stat != OILS_COPY_STATUS_RESHELVING )) {
1797 $copy->status( OILS_COPY_STATUS_RESHELVING );
1799 $self->checkin_changed(1);
1804 # Returns true if the item is at the current location
1805 # because it was transited there for a hold and the
1806 # hold has not been fulfilled
1807 sub checkin_check_holds_shelf {
1809 return 0 unless $self->copy;
1812 $U->copy_status($self->copy->status)->id ==
1813 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1815 # find the hold that put us on the holds shelf
1816 my $holds = $self->editor->search_action_hold_request(
1818 current_copy => $self->copy->id,
1819 capture_time => { '!=' => undef },
1820 fulfillment_time => undef,
1821 cancel_time => undef,
1826 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1827 $self->reshelve_copy(1);
1831 my $hold = $$holds[0];
1833 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1834 $hold->id. "] for copy ".$self->copy->barcode);
1836 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1837 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1841 $logger->info("circulator: hold is not for here..");
1842 $self->remote_hold($hold);
1847 sub checkin_handle_precat {
1849 my $copy = $self->copy;
1851 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1852 $copy->status(OILS_COPY_STATUS_CATALOGING);
1853 $self->update_copy();
1854 $self->checkin_changed(1);
1855 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1860 sub checkin_build_copy_transit {
1863 my $copy = $self->copy;
1864 my $transit = Fieldmapper::action::transit_copy->new;
1866 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1867 $logger->info("circulator: transiting copy to $dest");
1869 $transit->source($self->editor->requestor->ws_ou);
1870 $transit->dest($dest);
1871 $transit->target_copy($copy->id);
1872 $transit->source_send_time('now');
1873 $transit->copy_status( $U->copy_status($copy->status)->id );
1875 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1877 return $self->bail_on_events($self->editor->event)
1878 unless $self->editor->create_action_transit_copy($transit);
1880 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1882 $self->checkin_changed(1);
1886 sub attempt_checkin_hold_capture {
1888 my $copy = $self->copy;
1890 # See if this copy can fulfill any holds
1891 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1892 $self->editor, $copy, $self->editor->requestor );
1895 $logger->debug("circulator: no potential permitted".
1896 "holds found for copy ".$copy->barcode);
1900 $self->retarget($retarget);
1902 $logger->info("circulator: found permitted hold ".
1903 $hold->id . " for copy, capturing...");
1905 $hold->current_copy($copy->id);
1906 $hold->capture_time('now');
1908 # prevent DB errors caused by fetching
1909 # holds from storage, and updating through cstore
1910 $hold->clear_fulfillment_time;
1911 $hold->clear_fulfillment_staff;
1912 $hold->clear_fulfillment_lib;
1913 $hold->clear_expire_time;
1914 $hold->clear_cancel_time;
1915 $hold->clear_prev_check_time unless $hold->prev_check_time;
1917 $self->bail_on_events($self->editor->event)
1918 unless $self->editor->update_action_hold_request($hold);
1920 $self->checkin_changed(1);
1922 return 1 if $self->bail_out;
1924 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1926 # This hold was captured in the correct location
1927 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1928 $self->push_events(OpenILS::Event->new('SUCCESS'));
1930 #$self->do_hold_notify($hold->id);
1931 $self->notify_hold($hold->id);
1935 # Hold needs to be picked up elsewhere. Build a hold
1936 # transit and route the item.
1937 $self->checkin_build_hold_transit();
1938 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1939 return 1 if $self->bail_out;
1941 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1944 # make sure we save the copy status
1949 sub do_hold_notify {
1950 my( $self, $holdid ) = @_;
1952 $logger->info("circulator: running delayed hold notify process");
1954 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1955 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1957 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1958 hold_id => $holdid, requestor => $self->editor->requestor);
1960 $logger->debug("circulator: built hold notifier");
1962 if(!$notifier->event) {
1964 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1966 my $stat = $notifier->send_email_notify;
1967 if( $stat == '1' ) {
1968 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1972 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1975 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1979 sub retarget_holds {
1980 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1981 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1982 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1983 # no reason to wait for the return value
1987 sub checkin_build_hold_transit {
1990 my $copy = $self->copy;
1991 my $hold = $self->hold;
1992 my $trans = Fieldmapper::action::hold_transit_copy->new;
1994 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1996 $trans->hold($hold->id);
1997 $trans->source($self->editor->requestor->ws_ou);
1998 $trans->dest($hold->pickup_lib);
1999 $trans->source_send_time("now");
2000 $trans->target_copy($copy->id);
2002 # when the copy gets to its destination, it will recover
2003 # this status - put it onto the holds shelf
2004 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2006 return $self->bail_on_events($self->editor->event)
2007 unless $self->editor->create_action_hold_transit_copy($trans);
2012 sub process_received_transit {
2014 my $copy = $self->copy;
2015 my $copyid = $self->copy->id;
2017 my $status_name = $U->copy_status($copy->status)->name;
2018 $logger->debug("circulator: attempting transit receive on ".
2019 "copy $copyid. Copy status is $status_name");
2021 my $transit = $self->transit;
2023 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2024 # - this item is in-transit to a different location
2026 my $tid = $transit->id;
2027 my $loc = $self->editor->requestor->ws_ou;
2028 my $dest = $transit->dest;
2030 $logger->info("circulator: Fowarding transit on copy which is destined ".
2031 "for a different location. transit=$tid, copy=$copyid, current ".
2032 "location=$loc, destination location=$dest");
2034 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2036 # grab the associated hold object if available
2037 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2038 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2040 return $self->bail_on_events($evt);
2043 # The transit is received, set the receive time
2044 $transit->dest_recv_time('now');
2045 $self->bail_on_events($self->editor->event)
2046 unless $self->editor->update_action_transit_copy($transit);
2048 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2050 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2051 $copy->status( $transit->copy_status );
2052 $self->update_copy();
2053 return if $self->bail_out;
2057 #$self->do_hold_notify($hold_transit->hold);
2058 $self->notify_hold($hold_transit->hold);
2063 OpenILS::Event->new(
2066 payload => { transit => $transit, holdtransit => $hold_transit } ));
2068 return $hold_transit;
2072 sub checkin_handle_circ {
2076 my $circ = $self->circ;
2077 my $copy = $self->copy;
2081 # backdate the circ if necessary
2082 if($self->backdate) {
2083 $self->checkin_handle_backdate;
2084 return if $self->bail_out;
2087 if(!$circ->stop_fines) {
2088 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2089 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2090 $circ->stop_fines_time('now') unless $self->backdate;
2091 $circ->stop_fines_time($self->backdate) if $self->backdate;
2094 # see if there are any fines owed on this circ. if not, close it
2095 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2096 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2098 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2100 # Set the checkin vars since we have the item
2101 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2103 $circ->checkin_staff($self->editor->requestor->id);
2104 $circ->checkin_lib($self->editor->requestor->ws_ou);
2106 my $circ_lib = (ref $self->copy->circ_lib) ?
2107 $self->copy->circ_lib->id : $self->copy->circ_lib;
2108 my $stat = $U->copy_status($self->copy->status)->id;
2110 # If the item is lost/missing and it needs to be sent home, don't
2111 # reshelve the copy, leave it lost/missing so the recipient will know
2112 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2113 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2114 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2117 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2121 return $self->bail_on_events($self->editor->event)
2122 unless $self->editor->update_action_circulation($circ);
2126 sub checkin_handle_backdate {
2129 my $bd = $self->backdate;
2131 # ------------------------------------------------------------------
2132 # clean up the backdate for date comparison
2133 # we want any bills created on or after the backdate
2134 # ------------------------------------------------------------------
2135 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2136 #$bd = "${bd}T23:59:59";
2138 my $bills = $self->editor->search_money_billing(
2140 billing_ts => { '>=' => $bd },
2141 xact => $self->circ->id,
2142 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2146 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2148 for my $bill (@$bills) {
2149 unless( $U->is_true($bill->voided) ) {
2150 $logger->info("backdate voiding bill ".$bill->id);
2152 $bill->void_time('now');
2153 $bill->voider($self->editor->requestor->id);
2154 my $n = $bill->note || "";
2155 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2157 $self->bail_on_events($self->editor->event)
2158 unless $self->editor->update_money_billing($bill);
2166 sub find_patron_from_copy {
2168 my $circs = $self->editor->search_action_circulation(
2169 { target_copy => $self->copy->id, checkin_time => undef });
2170 my $circ = $circs->[0];
2171 return unless $circ;
2172 my $u = $self->editor->retrieve_actor_user($circ->usr)
2173 or return $self->bail_on_events($self->editor->event);
2177 sub check_checkin_copy_status {
2179 my $copy = $self->copy;
2185 my $status = $U->copy_status($copy->status)->id;
2188 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2189 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2190 $status == OILS_COPY_STATUS_IN_PROCESS ||
2191 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2192 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2193 $status == OILS_COPY_STATUS_CATALOGING ||
2194 $status == OILS_COPY_STATUS_RESHELVING );
2196 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2197 if( $status == OILS_COPY_STATUS_LOST );
2199 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2200 if( $status == OILS_COPY_STATUS_MISSING );
2202 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2207 # --------------------------------------------------------------------------
2208 # On checkin, we need to return as many relevant objects as we can
2209 # --------------------------------------------------------------------------
2210 sub checkin_flesh_events {
2213 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2214 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2215 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2219 for my $evt (@{$self->events}) {
2222 $payload->{copy} = $U->unflesh_copy($self->copy);
2223 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2224 $payload->{circ} = $self->circ;
2225 $payload->{transit} = $self->transit;
2226 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2228 # $self->hold may or may not have been replaced with a
2229 # valid hold after processing a cancelled hold
2230 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2232 $evt->{payload} = $payload;
2237 my( $self, $msg ) = @_;
2238 my $bc = ($self->copy) ? $self->copy->barcode :
2241 my $usr = ($self->patron) ? $self->patron->id : "";
2242 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2243 ", recipient=$usr, copy=$bc");
2249 $self->log_me("do_renew()");
2250 $self->is_renewal(1);
2252 # Make sure there is an open circ to renew that is not
2253 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2254 my $circ = $self->editor->search_action_circulation(
2255 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2258 $circ = $self->editor->search_action_circulation(
2260 target_copy => $self->copy->id,
2261 stop_fines => OILS_STOP_FINES_MAX_FINES,
2262 checkin_time => undef
2267 return $self->bail_on_events($self->editor->event) unless $circ;
2269 # A user is not allowed to renew another user's items without permission
2270 unless( $circ->usr eq $self->editor->requestor->id ) {
2271 return $self->bail_on_events($self->editor->events)
2272 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2275 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2276 if $circ->renewal_remaining < 1;
2278 # -----------------------------------------------------------------
2280 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2283 $self->run_renew_permit;
2286 $self->do_checkin();
2287 return if $self->bail_out;
2289 unless( $self->permit_override ) {
2291 return if $self->bail_out;
2292 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2293 $self->remove_event('ITEM_NOT_CATALOGED');
2296 $self->override_events;
2297 return if $self->bail_out;
2300 $self->do_checkout();
2305 my( $self, $evt ) = @_;
2306 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2307 $logger->debug("circulator: removing event from list: $evt");
2308 my @events = @{$self->events};
2309 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2314 my( $self, $evt ) = @_;
2315 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2316 return grep { $_->{textcode} eq $evt } @{$self->events};
2321 sub run_renew_permit {
2326 if(!$self->legacy_script_support) {
2327 my $results = $self->run_indb_circ_test;
2328 unless($self->circ_test_success) {
2329 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2334 my $runner = $self->script_runner;
2336 $runner->load($self->circ_permit_renew);
2337 my $result = $runner->run or
2338 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2339 my $events = $result->{events};
2342 $logger->activity("ciculator: circ_permit_renew for user ".
2343 $self->patron->id." returned events: @$events") if @$events;
2345 $self->push_events(OpenILS::Event->new($_)) for @$events;
2347 $logger->debug("circulator: re-creating script runner to be safe");
2348 #$self->mk_script_runner;