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 $circulator->do_permit();
198 unless( $circulator->bail_out ) {
199 $circulator->events([]);
200 $circulator->do_checkout();
203 } elsif( $api =~ /inspect/ ) {
204 my $data = $circulator->do_inspect();
205 $circulator->editor->rollback;
208 } elsif( $api =~ /checkout/ ) {
209 $circulator->do_checkout();
211 } elsif( $api =~ /checkin/ ) {
212 $circulator->do_checkin();
214 } elsif( $api =~ /renew/ ) {
215 $circulator->is_renewal(1);
216 $circulator->do_renew();
219 if( $circulator->bail_out ) {
222 # make sure no success event accidentally slip in
224 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
227 my @e = @{$circulator->events};
228 push( @ee, $_->{textcode} ) for @e;
229 $logger->info("circulator: bailing out with events: @ee");
231 $circulator->editor->rollback;
234 $circulator->editor->commit;
237 $circulator->script_runner->cleanup if $circulator->script_runner;
239 $conn->respond_complete(circ_events($circulator));
241 unless($circulator->bail_out) {
242 $circulator->do_hold_notify($circulator->notify_hold)
243 if $circulator->notify_hold;
244 $circulator->retarget_holds if $circulator->retarget;
250 my @e = @{$circ->events};
251 # if we have multiple events, SUCCESS should not be one of them;
252 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
253 return (@e == 1) ? $e[0] : \@e;
257 sub translate_legacy_args {
260 if( $$args{barcode} ) {
261 $$args{copy_barcode} = $$args{barcode};
262 delete $$args{barcode};
265 if( $$args{copyid} ) {
266 $$args{copy_id} = $$args{copyid};
267 delete $$args{copyid};
270 if( $$args{patronid} ) {
271 $$args{patron_id} = $$args{patronid};
272 delete $$args{patronid};
275 if( $$args{patron} and !ref($$args{patron}) ) {
276 $$args{patron_id} = $$args{patron};
277 delete $$args{patron};
281 if( $$args{noncat} ) {
282 $$args{is_noncat} = $$args{noncat};
283 delete $$args{noncat};
286 if( $$args{precat} ) {
287 $$args{is_precat} = $$args{request_precat} = $$args{precat};
288 delete $$args{precat};
294 # --------------------------------------------------------------------------
295 # This package actually manages all of the circulation logic
296 # --------------------------------------------------------------------------
297 package OpenILS::Application::Circ::Circulator;
298 use strict; use warnings;
299 use vars q/$AUTOLOAD/;
301 use OpenILS::Utils::Fieldmapper;
302 use OpenSRF::Utils::Cache;
303 use Digest::MD5 qw(md5_hex);
304 use DateTime::Format::ISO8601;
305 use OpenILS::Utils::PermitHold;
306 use OpenSRF::Utils qw/:datetime/;
307 use OpenSRF::Utils::SettingsClient;
308 use OpenILS::Application::Circ::Holds;
309 use OpenILS::Application::Circ::Transit;
310 use OpenSRF::Utils::Logger qw(:logger);
311 use OpenILS::Utils::CStoreEditor qw/:funcs/;
312 use OpenILS::Application::Circ::ScriptBuilder;
313 use OpenILS::Const qw/:const/;
315 my $holdcode = "OpenILS::Application::Circ::Holds";
316 my $transcode = "OpenILS::Application::Circ::Transit";
321 # --------------------------------------------------------------------------
322 # Add a pile of automagic getter/setter methods
323 # --------------------------------------------------------------------------
324 my @AUTOLOAD_FIELDS = qw/
339 check_penalty_on_renew
367 recurring_fines_level
380 cancelled_hold_transit
389 legacy_script_support
401 my $type = ref($self) or die "$self is not an object";
403 my $name = $AUTOLOAD;
406 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
407 $logger->error("circulator: $type: invalid autoload field: $name");
408 die "$type: invalid autoload field: $name\n"
413 *{"${type}::${name}"} = sub {
416 $s->{$name} = $v if defined $v;
420 return $self->$name($data);
425 my( $class, $auth, %args ) = @_;
426 $class = ref($class) || $class;
427 my $self = bless( {}, $class );
430 $self->editor(new_editor(xact => 1, authtoken => $auth));
432 unless( $self->editor->checkauth ) {
433 $self->bail_on_events($self->editor->event);
437 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
439 $self->$_($args{$_}) for keys %args;
442 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
444 # if this is a renewal, default to desk_renewal
445 $self->desk_renewal(1) unless
446 $self->opac_renewal or $self->phone_renewal;
448 $self->capture('') unless $self->capture;
454 # --------------------------------------------------------------------------
455 # True if we should discontinue processing
456 # --------------------------------------------------------------------------
458 my( $self, $bool ) = @_;
459 if( defined $bool ) {
460 $logger->info("circulator: BAILING OUT") if $bool;
461 $self->{bail_out} = $bool;
463 return $self->{bail_out};
468 my( $self, @evts ) = @_;
471 $logger->info("circulator: pushing event ".$e->{textcode});
472 push( @{$self->events}, $e ) unless
473 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
479 my $key = md5_hex( time() . rand() . "$$" );
480 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
481 return $self->permit_key($key);
484 sub check_permit_key {
486 my $key = $self->permit_key;
487 return 0 unless $key;
488 my $k = "oils_permit_key_$key";
489 my $one = $self->cache_handle->get_cache($k);
490 $self->cache_handle->delete_cache($k);
491 return ($one) ? 1 : 0;
496 my $e = $self->editor;
498 # --------------------------------------------------------------------------
499 # Grab the fleshed copy
500 # --------------------------------------------------------------------------
501 unless($self->is_noncat) {
505 flesh_fields => {acp => ['call_number'], acn => ['record']}
508 $copy = $e->retrieve_asset_copy(
509 [$self->copy_id, $flesh ]) or return $e->event;
511 } elsif( $self->copy_barcode ) {
513 $copy = $e->search_asset_copy(
514 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
519 $self->volume($copy->call_number);
520 $self->title($self->volume->record);
521 $self->copy->call_number($self->volume->id);
522 $self->volume->record($self->title->id);
523 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
524 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
525 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
526 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
529 # We can't renew if there is no copy
530 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
531 if $self->is_renewal;
536 return undef if $self->is_checkin;
538 # --------------------------------------------------------------------------
540 # --------------------------------------------------------------------------
542 if( $self->patron_id ) {
543 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
545 } elsif( $self->patron_barcode ) {
547 my $card = $e->search_actor_card(
548 {barcode => $self->patron_barcode})->[0] or return $e->event;
550 $patron = $e->search_actor_user(
551 {card => $card->id})->[0] or return $e->event;
554 if( my $copy = $self->copy ) {
555 my $circs = $e->search_action_circulation(
556 {target_copy => $copy->id, checkin_time => undef});
558 if( my $circ = $circs->[0] ) {
559 $patron = $e->retrieve_actor_user($circ->usr)
565 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
566 unless $self->patron($patron);
569 # --------------------------------------------------------------------------
570 # This builds the script runner environment and fetches most of the
572 # --------------------------------------------------------------------------
573 sub mk_script_runner {
579 qw/copy copy_barcode copy_id patron
580 patron_id patron_barcode volume title editor/;
582 # Translate our objects into the ScriptBuilder args hash
583 $$args{$_} = $self->$_() for @fields;
585 $args->{ignore_user_status} = 1 if $self->is_checkin;
586 $$args{fetch_patron_by_circ_copy} = 1;
587 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
589 if( my $pco = $self->pending_checkouts ) {
590 $logger->info("circulator: we were given a pending checkouts number of $pco");
591 $$args{patronItemsOut} = $pco;
594 # This fetches most of the objects we need
595 $self->script_runner(
596 OpenILS::Application::Circ::ScriptBuilder->build($args));
598 # Now we translate the ScriptBuilder objects back into self
599 $self->$_($$args{$_}) for @fields;
601 my @evts = @{$args->{_events}} if $args->{_events};
603 $logger->debug("circulator: script builder returned events: @evts") if @evts;
607 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
608 if(!$self->is_noncat and
610 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
614 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
615 return $self->bail_on_events(@e);
620 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
621 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
622 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
623 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
627 # We can't renew if there is no copy
628 return $self->bail_on_events(@evts) if
629 $self->is_renewal and !$self->copy;
631 # Set some circ-specific flags in the script environment
632 my $evt = "environment";
633 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
635 if( $self->is_noncat ) {
636 $self->script_runner->insert("$evt.isNonCat", 1);
637 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
640 if( $self->is_precat ) {
641 $self->script_runner->insert("environment.isPrecat", 1, 1);
644 $self->script_runner->add_path( $_ ) for @$script_libs;
649 # --------------------------------------------------------------------------
650 # Does the circ permit work
651 # --------------------------------------------------------------------------
655 $self->log_me("do_permit()");
657 unless( $self->editor->requestor->id == $self->patron->id ) {
658 return $self->bail_on_events($self->editor->event)
659 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
662 $self->check_captured_holds();
663 $self->do_copy_checks();
664 return if $self->bail_out;
665 $self->run_patron_permit_scripts();
666 $self->run_copy_permit_scripts()
667 unless $self->is_precat or $self->is_noncat;
668 $self->check_item_deposit_events();
669 $self->override_events() unless
670 $self->is_renewal and not $self->check_penalty_on_renew;
671 return if $self->bail_out;
673 if($self->is_precat and not $self->request_precat) {
676 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
677 return $self->bail_out(1) unless $self->is_renewal;
683 payload => $self->mk_permit_key));
686 sub check_item_deposit_events {
688 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED'))
689 if $self->is_deposit and not $self->is_deposit_exempt;
690 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED'))
691 if $self->is_rental and not $self->is_rental_exempt;
694 # returns true if the user is not required to pay deposits
695 sub is_deposit_exempt {
697 my $pid = (ref $self->patron->profile) ?
698 $self->patron->profile->id : $self->patron->profile;
699 my $groups = $U->ou_ancestor_setting_value(
700 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
701 return 1 if $groups and grep {$_ == $pid} @$groups;
705 # returns true if the user is not required to pay rental fees
706 sub is_rental_exempt {
708 my $pid = (ref $self->patron->profile) ?
709 $self->patron->profile->id : $self->patron->profile;
710 my $groups = $U->ou_ancestor_setting_value(
711 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
712 return 1 if $groups and grep {$_ == $pid} @$groups;
716 sub check_captured_holds {
718 my $copy = $self->copy;
719 my $patron = $self->patron;
721 return undef unless $copy;
723 my $s = $U->copy_status($copy->status)->id;
724 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
725 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
727 # Item is on the holds shelf, make sure it's going to the right person
728 my $holds = $self->editor->search_action_hold_request(
731 current_copy => $copy->id ,
732 capture_time => { '!=' => undef },
733 cancel_time => undef,
734 fulfillment_time => undef
740 if( $holds and $$holds[0] ) {
741 return undef if $$holds[0]->usr == $patron->id;
744 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
746 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
752 my $copy = $self->copy;
755 my $stat = $U->copy_status($copy->status)->id;
757 # We cannot check out a copy if it is in-transit
758 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
759 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
762 $self->handle_claims_returned();
763 return if $self->bail_out;
765 # no claims returned circ was found, check if there is any open circ
766 unless( $self->is_renewal ) {
767 my $circs = $self->editor->search_action_circulation(
768 { target_copy => $copy->id, checkin_time => undef }
771 return $self->bail_on_events(
772 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
777 sub send_penalty_request {
779 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
780 $self->penalty_request(
782 'open-ils.penalty.patron_penalty.calculate',
784 authtoken => $self->editor->authtoken,
785 patron => $self->patron } ) );
788 sub gather_penalty_request {
790 return [] unless $self->penalty_request;
791 my $data = $self->penalty_request->recv;
793 throw $data if UNIVERSAL::isa($data,'Error');
794 $data = $data->content;
795 return $data->{fatal_penalties};
797 $logger->error("circulator: penalty request returned no data");
801 my $LEGACY_CIRC_EVENT_MAP = {
802 'actor.usr.barred' => 'PATRON_BARRED',
803 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
804 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
805 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
806 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
807 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
808 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
809 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
810 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
814 # ---------------------------------------------------------------------
815 # This pushes any patron-related events into the list but does not
816 # set bail_out for any events
817 # ---------------------------------------------------------------------
818 sub run_patron_permit_scripts {
820 my $runner = $self->script_runner;
821 my $patronid = $self->patron->id;
825 if(!$self->legacy_script_support) {
827 my $results = $self->run_indb_circ_test;
828 unless($self->circ_test_success) {
829 push(@allevents, OpenILS::Event->new(
830 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
835 $self->send_penalty_request() unless
836 $self->is_renewal and not $self->check_penalty_on_renew;
839 # --------------------------------------------------------------------- # Now run the patron permit script
840 # ---------------------------------------------------------------------
841 $runner->load($self->circ_permit_patron);
842 my $result = $runner->run or
843 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
845 my $patron_events = $result->{events};
847 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
848 [] : $self->gather_penalty_request();
850 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
853 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
855 $self->push_events(@allevents);
858 sub run_indb_circ_test {
860 return $self->matrix_test_result if $self->matrix_test_result;
862 my $dbfunc = ($self->is_renewal) ?
863 'action.item_user_renew_test' : 'action.item_user_circ_test';
865 my $results = ($self->matrix_test_result) ?
866 $self->matrix_test_result :
867 $self->editor->json_query(
870 $self->editor->requestor->ws_ou,
871 ($self->is_precat) ? undef : $self->copy->id,
877 $self->circ_test_success($U->is_true($results->[0]->{success}));
878 if($self->circ_test_success) {
879 $self->circ_matrix_test(
880 $self->editor->retrieve_config_circ_matrix_test(
881 $results->[0]->{matchpoint}
886 if($self->circ_test_success) {
887 $self->circ_matrix_ruleset(
888 $self->editor->retrieve_config_circ_matrix_ruleset([
889 $results->[0]->{matchpoint},
892 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
900 return $self->matrix_test_result($results);
905 $self->run_indb_circ_test;
908 circ_test_success => $self->circ_test_success,
909 failure_events => [],
913 unless($self->circ_test_success) {
914 push(@{$results->{failure_codes}},
915 $_->{fail_part}) for @{$self->matrix_test_result};
916 push(@{$results->{failure_events}},
917 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
918 for @{$self->matrix_test_result};
922 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
923 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
924 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
926 my $policy = $self->get_circ_policy(
927 $duration_rule, $recurring_fine_rule, $max_fine_rule);
929 $$results{$_} = $$policy{$_} for keys %$policy;
933 # ---------------------------------------------------------------------
934 # Loads the circ policy info for duration, recurring fine, and max
935 # fine based on the current copy
936 # ---------------------------------------------------------------------
937 sub get_circ_policy {
938 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
941 duration_rule => $duration_rule->name,
942 recurring_fine_rule => $recurring_fine_rule->name,
943 max_fine_rule => $max_fine_rule->name,
944 max_fine => $self->get_max_fine_amount($max_fine_rule),
945 fine_interval => $recurring_fine_rule->recurance_interval,
946 renewal_remaining => $duration_rule->max_renewals
949 $policy->{duration} = $duration_rule->shrt
950 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
951 $policy->{duration} = $duration_rule->normal
952 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
953 $policy->{duration} = $duration_rule->extended
954 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
956 $policy->{recurring_fine} = $recurring_fine_rule->low
957 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
958 $policy->{recurring_fine} = $recurring_fine_rule->normal
959 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
960 $policy->{recurring_fine} = $recurring_fine_rule->high
961 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
966 sub get_max_fine_amount {
968 my $max_fine_rule = shift;
969 my $max_amount = $max_fine_rule->amount;
971 # if is_percent is true then the max->amount is
972 # use as a percentage of the copy price
973 if ($U->is_true($max_fine_rule->is_percent)) {
974 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
975 $max_amount = $price * $max_fine_rule->amount / 100;
983 sub run_copy_permit_scripts {
985 my $copy = $self->copy || return;
986 my $runner = $self->script_runner;
990 if(!$self->legacy_script_support) {
991 my $results = $self->run_indb_circ_test;
992 unless($self->circ_test_success) {
993 push(@allevents, OpenILS::Event->new(
994 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
998 # ---------------------------------------------------------------------
999 # Capture all of the copy permit events
1000 # ---------------------------------------------------------------------
1001 $runner->load($self->circ_permit_copy);
1002 my $result = $runner->run or
1003 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1004 my $copy_events = $result->{events};
1006 # ---------------------------------------------------------------------
1007 # Now collect all of the events together
1008 # ---------------------------------------------------------------------
1009 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1012 # See if this copy has an alert message
1013 my $ae = $self->check_copy_alert();
1014 push( @allevents, $ae ) if $ae;
1016 # uniquify the events
1017 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1018 @allevents = values %hash;
1021 $_->{payload} = $copy if
1022 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1025 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1027 $self->push_events(@allevents);
1031 sub check_copy_alert {
1033 return undef if $self->is_renewal;
1034 return OpenILS::Event->new(
1035 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1036 if $self->copy and $self->copy->alert_message;
1042 # --------------------------------------------------------------------------
1043 # If the call is overriding and has permissions to override every collected
1044 # event, the are cleared. Any event that the caller does not have
1045 # permission to override, will be left in the event list and bail_out will
1047 # XXX We need code in here to cancel any holds/transits on copies
1048 # that are being force-checked out
1049 # --------------------------------------------------------------------------
1050 sub override_events {
1052 my @events = @{$self->events};
1053 return unless @events;
1055 if(!$self->override) {
1056 return $self->bail_out(1)
1057 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1062 for my $e (@events) {
1063 my $tc = $e->{textcode};
1064 next if $tc eq 'SUCCESS';
1065 my $ov = "$tc.override";
1066 $logger->info("circulator: attempting to override event: $ov");
1068 return $self->bail_on_events($self->editor->event)
1069 unless( $self->editor->allowed($ov) );
1074 # --------------------------------------------------------------------------
1075 # If there is an open claimsreturn circ on the requested copy, close the
1076 # circ if overriding, otherwise bail out
1077 # --------------------------------------------------------------------------
1078 sub handle_claims_returned {
1080 my $copy = $self->copy;
1082 my $CR = $self->editor->search_action_circulation(
1084 target_copy => $copy->id,
1085 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1086 checkin_time => undef,
1090 return unless ($CR = $CR->[0]);
1094 # - If the caller has set the override flag, we will check the item in
1095 if($self->override) {
1097 $CR->checkin_time('now');
1098 $CR->checkin_lib($self->editor->requestor->ws_ou);
1099 $CR->checkin_staff($self->editor->requestor->id);
1101 $evt = $self->editor->event
1102 unless $self->editor->update_action_circulation($CR);
1105 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1108 $self->bail_on_events($evt) if $evt;
1113 # --------------------------------------------------------------------------
1114 # This performs the checkout
1115 # --------------------------------------------------------------------------
1119 $self->log_me("do_checkout()");
1121 # make sure perms are good if this isn't a renewal
1122 unless( $self->is_renewal ) {
1123 return $self->bail_on_events($self->editor->event)
1124 unless( $self->editor->allowed('COPY_CHECKOUT') );
1127 # verify the permit key
1128 unless( $self->check_permit_key ) {
1129 if( $self->permit_override ) {
1130 return $self->bail_on_events($self->editor->event)
1131 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1133 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1137 # if this is a non-cataloged circ, build the circ and finish
1138 if( $self->is_noncat ) {
1139 $self->checkout_noncat;
1141 OpenILS::Event->new('SUCCESS',
1142 payload => { noncat_circ => $self->circ }));
1146 if( $self->is_precat ) {
1147 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1148 $self->make_precat_copy;
1149 return if $self->bail_out;
1151 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1152 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1155 $self->do_copy_checks;
1156 return if $self->bail_out;
1158 $self->run_checkout_scripts();
1159 return if $self->bail_out;
1161 $self->build_checkout_circ_object();
1162 return if $self->bail_out;
1164 $self->apply_modified_due_date();
1165 return if $self->bail_out;
1167 return $self->bail_on_events($self->editor->event)
1168 unless $self->editor->create_action_circulation($self->circ);
1170 # refresh the circ to force local time zone for now
1171 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1173 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1175 return if $self->bail_out;
1177 $self->apply_deposit_fee();
1178 return if $self->bail_out;
1180 $self->handle_checkout_holds();
1181 return if $self->bail_out;
1183 # ------------------------------------------------------------------------------
1184 # Update the patron penalty info in the DB. Run it for permit-overrides or
1185 # renewals since both of those cases do not require the penalty server to
1186 # run during the permit phase of the checkout
1187 # ------------------------------------------------------------------------------
1188 if( $self->permit_override or $self->is_renewal ) {
1189 $U->update_patron_penalties(
1190 authtoken => $self->editor->authtoken,
1191 patron => $self->patron,
1196 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1198 OpenILS::Event->new('SUCCESS',
1200 copy => $U->unflesh_copy($self->copy),
1201 circ => $self->circ,
1203 holds_fulfilled => $self->fulfilled_holds,
1204 deposit_billing => $self->deposit_billing,
1205 rental_billing => $self->rental_billing
1211 sub apply_deposit_fee {
1213 my $copy = $self->copy;
1215 ($self->is_deposit and not $self->is_deposit_exempt) or
1216 ($self->is_rental and not $self->is_rental_exempt);
1218 my $bill = Fieldmapper::money::billing->new;
1219 my $amount = $copy->deposit_amount;
1222 if($self->is_deposit) {
1223 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1224 $self->deposit_billing($bill);
1226 $billing_type = OILS_BILLING_TYPE_RENTAL;
1227 $self->rental_billing($bill);
1230 $bill->xact($self->circ->id);
1231 $bill->amount($amount);
1232 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1233 $bill->billing_type($billing_type);
1234 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1236 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1241 my $copy = $self->copy;
1243 my $stat = $copy->status if ref $copy->status;
1244 my $loc = $copy->location if ref $copy->location;
1245 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1247 $copy->status($stat->id) if $stat;
1248 $copy->location($loc->id) if $loc;
1249 $copy->circ_lib($circ_lib->id) if $circ_lib;
1250 $copy->editor($self->editor->requestor->id);
1251 $copy->edit_date('now');
1252 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1254 return $self->bail_on_events($self->editor->event)
1255 unless $self->editor->update_asset_copy($self->copy);
1257 $copy->status($U->copy_status($copy->status));
1258 $copy->location($loc) if $loc;
1259 $copy->circ_lib($circ_lib) if $circ_lib;
1263 sub bail_on_events {
1264 my( $self, @evts ) = @_;
1265 $self->push_events(@evts);
1269 sub handle_checkout_holds {
1272 my $copy = $self->copy;
1273 my $patron = $self->patron;
1275 my $holds = $self->editor->search_action_hold_request(
1277 current_copy => $copy->id ,
1278 cancel_time => undef,
1279 fulfillment_time => undef
1285 # XXX We should only fulfill one hold here...
1286 # XXX If a hold was transited to the user who is checking out
1287 # the item, we need to make sure that hold is what's grabbed
1290 # for now, just sort by id to get what should be the oldest hold
1291 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1292 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1293 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1296 my $hold = $myholds[0];
1298 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1300 # if the hold was never officially captured, capture it.
1301 $hold->capture_time('now') unless $hold->capture_time;
1303 # just make sure it's set correctly
1304 $hold->current_copy($copy->id);
1306 $hold->fulfillment_time('now');
1307 $hold->fulfillment_staff($self->editor->requestor->id);
1308 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1310 return $self->bail_on_events($self->editor->event)
1311 unless $self->editor->update_action_hold_request($hold);
1313 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1315 push( @fulfilled, $hold->id );
1318 # If there are any holds placed for other users that point to this copy,
1319 # then we need to un-target those holds so the targeter can pick a new copy
1322 $logger->info("circulator: un-targeting hold ".$_->id.
1323 " because copy ".$copy->id." is getting checked out");
1325 # - make the targeter process this hold at next run
1326 $_->clear_prev_check_time;
1328 # - clear out the targetted copy
1329 $_->clear_current_copy;
1330 $_->clear_capture_time;
1332 return $self->bail_on_event($self->editor->event)
1333 unless $self->editor->update_action_hold_request($_);
1337 $self->fulfilled_holds(\@fulfilled);
1342 sub run_checkout_scripts {
1346 my $runner = $self->script_runner;
1355 if(!$self->legacy_script_support) {
1356 $self->run_indb_circ_test();
1357 $duration = $self->circ_matrix_ruleset->duration_rule;
1358 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1359 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1363 $runner->load($self->circ_duration);
1365 my $result = $runner->run or
1366 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1368 $duration_name = $result->{durationRule};
1369 $recurring_name = $result->{recurringFinesRule};
1370 $max_fine_name = $result->{maxFine};
1373 $duration_name = $duration->name if $duration;
1374 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1377 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1378 return $self->bail_on_events($evt) if $evt;
1380 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1381 return $self->bail_on_events($evt) if $evt;
1383 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1384 return $self->bail_on_events($evt) if $evt;
1389 # The item circulates with an unlimited duration
1395 $self->duration_rule($duration);
1396 $self->recurring_fines_rule($recurring);
1397 $self->max_fine_rule($max_fine);
1401 sub build_checkout_circ_object {
1404 my $circ = Fieldmapper::action::circulation->new;
1405 my $duration = $self->duration_rule;
1406 my $max = $self->max_fine_rule;
1407 my $recurring = $self->recurring_fines_rule;
1408 my $copy = $self->copy;
1409 my $patron = $self->patron;
1413 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1415 my $dname = $duration->name;
1416 my $mname = $max->name;
1417 my $rname = $recurring->name;
1419 $logger->debug("circulator: building circulation ".
1420 "with duration=$dname, maxfine=$mname, recurring=$rname");
1422 $circ->duration($policy->{duration});
1423 $circ->recuring_fine($policy->{recurring_fine});
1424 $circ->duration_rule($duration->name);
1425 $circ->recuring_fine_rule($recurring->name);
1426 $circ->max_fine_rule($max->name);
1427 $circ->max_fine($policy->{max_fine});
1428 $circ->fine_interval($recurring->recurance_interval);
1429 $circ->renewal_remaining($duration->max_renewals);
1433 $logger->info("circulator: copy found with an unlimited circ duration");
1434 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1435 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1436 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1437 $circ->renewal_remaining(0);
1440 $circ->target_copy( $copy->id );
1441 $circ->usr( $patron->id );
1442 $circ->circ_lib( $self->circ_lib );
1444 if( $self->is_renewal ) {
1445 $circ->opac_renewal('t') if $self->opac_renewal;
1446 $circ->phone_renewal('t') if $self->phone_renewal;
1447 $circ->desk_renewal('t') if $self->desk_renewal;
1448 $circ->renewal_remaining($self->renewal_remaining);
1449 $circ->circ_staff($self->editor->requestor->id);
1453 # if the user provided an overiding checkout time,
1454 # (e.g. the checkout really happened several hours ago), then
1455 # we apply that here. Does this need a perm??
1456 $circ->xact_start(clense_ISO8601($self->checkout_time))
1457 if $self->checkout_time;
1459 # if a patron is renewing, 'requestor' will be the patron
1460 $circ->circ_staff($self->editor->requestor->id);
1461 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1467 sub apply_modified_due_date {
1469 my $circ = $self->circ;
1470 my $copy = $self->copy;
1472 if( $self->due_date ) {
1474 return $self->bail_on_events($self->editor->event)
1475 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1477 $circ->due_date(clense_ISO8601($self->due_date));
1481 # if the due_date lands on a day when the location is closed
1482 return unless $copy and $circ->due_date;
1484 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1486 # due-date overlap should be determined by the location the item
1487 # is checked out from, not the owning or circ lib of the item
1488 my $org = $self->editor->requestor->ws_ou;
1490 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1491 " with an item due date of ".$circ->due_date );
1493 my $dateinfo = $U->storagereq(
1494 'open-ils.storage.actor.org_unit.closed_date.overlap',
1495 $org, $circ->due_date );
1498 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1499 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1501 # XXX make the behavior more dynamic
1502 # for now, we just push the due date to after the close date
1503 $circ->due_date($dateinfo->{end});
1510 sub create_due_date {
1511 my( $self, $duration ) = @_;
1512 # if there is a raw time component (e.g. from postgres),
1513 # turn it into an interval that interval_to_seconds can parse
1514 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1515 my ($sec,$min,$hour,$mday,$mon,$year) =
1516 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1517 $year += 1900; $mon += 1;
1518 my $due_date = sprintf(
1519 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1520 $year, $mon, $mday, $hour, $min, $sec);
1526 sub make_precat_copy {
1528 my $copy = $self->copy;
1531 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1533 $copy->editor($self->editor->requestor->id);
1534 $copy->edit_date('now');
1535 $copy->dummy_title($self->dummy_title);
1536 $copy->dummy_author($self->dummy_author);
1538 $self->update_copy();
1542 $logger->info("circulator: Creating a new precataloged ".
1543 "copy in checkout with barcode " . $self->copy_barcode);
1545 $copy = Fieldmapper::asset::copy->new;
1546 $copy->circ_lib($self->circ_lib);
1547 $copy->creator($self->editor->requestor->id);
1548 $copy->editor($self->editor->requestor->id);
1549 $copy->barcode($self->copy_barcode);
1550 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1551 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1552 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1554 $copy->dummy_title($self->dummy_title || "");
1555 $copy->dummy_author($self->dummy_author || "");
1557 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1559 $self->push_events($self->editor->event);
1563 # this is a little bit of a hack, but we need to
1564 # get the copy into the script runner
1565 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1569 sub checkout_noncat {
1575 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1576 my $count = $self->noncat_count || 1;
1577 my $cotime = clense_ISO8601($self->checkout_time) || "";
1579 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1583 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1584 $self->editor->requestor->id,
1592 $self->push_events($evt);
1603 $self->log_me("do_checkin()");
1606 return $self->bail_on_events(
1607 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1610 if( $self->checkin_check_holds_shelf() ) {
1611 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1612 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1613 $self->checkin_flesh_events;
1617 unless( $self->is_renewal ) {
1618 return $self->bail_on_events($self->editor->event)
1619 unless $self->editor->allowed('COPY_CHECKIN');
1622 $self->push_events($self->check_copy_alert());
1623 $self->push_events($self->check_checkin_copy_status());
1625 # the renew code will have already found our circulation object
1626 unless( $self->is_renewal and $self->circ ) {
1627 my $circs = $self->editor->search_action_circulation(
1628 { target_copy => $self->copy->id, checkin_time => undef });
1629 $self->circ($$circs[0]);
1631 # for now, just warn if there are multiple open circs on a copy
1632 $logger->warn("circulator: we have ".scalar(@$circs).
1633 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1636 # if the circ is marked as 'claims returned', add the event to the list
1637 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1638 if ($self->circ and $self->circ->stop_fines
1639 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1641 $self->check_circ_deposit();
1643 # handle the overridable events
1644 $self->override_events unless $self->is_renewal;
1645 return if $self->bail_out;
1649 $self->editor->search_action_transit_copy(
1650 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1654 $self->checkin_handle_circ;
1655 return if $self->bail_out;
1656 $self->checkin_changed(1);
1658 } elsif( $self->transit ) {
1659 my $hold_transit = $self->process_received_transit;
1660 $self->checkin_changed(1);
1662 if( $self->bail_out ) {
1663 $self->checkin_flesh_events;
1667 if( my $e = $self->check_checkin_copy_status() ) {
1668 # If the original copy status is special, alert the caller
1669 my $ev = $self->events;
1670 $self->events([$e]);
1671 $self->override_events;
1672 return if $self->bail_out;
1676 if( $hold_transit or
1677 $U->copy_status($self->copy->status)->id
1678 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1681 if( $hold_transit ) {
1682 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1684 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1689 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1691 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1692 $self->reshelve_copy(1);
1693 $self->cancelled_hold_transit(1);
1694 $self->notify_hold(0); # don't notify for cancelled holds
1695 return if $self->bail_out;
1699 # hold transited to correct location
1700 $self->checkin_flesh_events;
1705 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1707 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1708 " that is in-transit, but there is no transit.. repairing");
1709 $self->reshelve_copy(1);
1710 return if $self->bail_out;
1713 if( $self->is_renewal ) {
1714 $self->push_events(OpenILS::Event->new('SUCCESS'));
1718 # ------------------------------------------------------------------------------
1719 # Circulations and transits are now closed where necessary. Now go on to see if
1720 # this copy can fulfill a hold or needs to be routed to a different location
1721 # ------------------------------------------------------------------------------
1723 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1725 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1726 return if $self->bail_out;
1728 unless($needed_for_hold) {
1729 my $circ_lib = (ref $self->copy->circ_lib) ?
1730 $self->copy->circ_lib->id : $self->copy->circ_lib;
1732 if( $self->remote_hold ) {
1733 $circ_lib = $self->remote_hold->pickup_lib;
1734 $logger->warn("circulator: Copy ".$self->copy->barcode.
1735 " is on a remote hold's shelf, sending to $circ_lib");
1738 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1740 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1742 $self->checkin_handle_precat();
1743 return if $self->bail_out;
1747 my $bc = $self->copy->barcode;
1748 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1749 $self->checkin_build_copy_transit($circ_lib);
1750 return if $self->bail_out;
1751 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1756 $self->reshelve_copy;
1757 return if $self->bail_out;
1759 unless($self->checkin_changed) {
1761 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1762 my $stat = $U->copy_status($self->copy->status)->id;
1764 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1765 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1766 $self->bail_out(1); # no need to commit anything
1770 $self->push_events(OpenILS::Event->new('SUCCESS'))
1771 unless @{$self->events};
1775 # ------------------------------------------------------------------------------
1776 # Update the patron penalty info in the DB
1777 # ------------------------------------------------------------------------------
1778 $U->update_patron_penalties(
1779 authtoken => $self->editor->authtoken,
1780 patron => $self->patron,
1781 background => 1 ) if $self->is_checkin;
1783 $self->checkin_flesh_events;
1787 # if a deposit was payed for this item, push the event
1788 sub check_circ_deposit {
1790 return unless $self->circ;
1791 my $deposit = $self->editor->search_money_billing(
1792 { billing_type => OILS_BILLING_TYPE_DEPOSIT,
1793 xact => $self->circ->id,
1795 }, {idlist => 1})->[0];
1797 $self->push_events(OpenILS::Event->new(
1798 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1803 my $force = $self->force || shift;
1804 my $copy = $self->copy;
1806 my $stat = $U->copy_status($copy->status)->id;
1809 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1810 $stat != OILS_COPY_STATUS_CATALOGING and
1811 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1812 $stat != OILS_COPY_STATUS_RESHELVING )) {
1814 $copy->status( OILS_COPY_STATUS_RESHELVING );
1816 $self->checkin_changed(1);
1821 # Returns true if the item is at the current location
1822 # because it was transited there for a hold and the
1823 # hold has not been fulfilled
1824 sub checkin_check_holds_shelf {
1826 return 0 unless $self->copy;
1829 $U->copy_status($self->copy->status)->id ==
1830 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1832 # find the hold that put us on the holds shelf
1833 my $holds = $self->editor->search_action_hold_request(
1835 current_copy => $self->copy->id,
1836 capture_time => { '!=' => undef },
1837 fulfillment_time => undef,
1838 cancel_time => undef,
1843 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1844 $self->reshelve_copy(1);
1848 my $hold = $$holds[0];
1850 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1851 $hold->id. "] for copy ".$self->copy->barcode);
1853 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1854 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1858 $logger->info("circulator: hold is not for here..");
1859 $self->remote_hold($hold);
1864 sub checkin_handle_precat {
1866 my $copy = $self->copy;
1868 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1869 $copy->status(OILS_COPY_STATUS_CATALOGING);
1870 $self->update_copy();
1871 $self->checkin_changed(1);
1872 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1877 sub checkin_build_copy_transit {
1880 my $copy = $self->copy;
1881 my $transit = Fieldmapper::action::transit_copy->new;
1883 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1884 $logger->info("circulator: transiting copy to $dest");
1886 $transit->source($self->editor->requestor->ws_ou);
1887 $transit->dest($dest);
1888 $transit->target_copy($copy->id);
1889 $transit->source_send_time('now');
1890 $transit->copy_status( $U->copy_status($copy->status)->id );
1892 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1894 return $self->bail_on_events($self->editor->event)
1895 unless $self->editor->create_action_transit_copy($transit);
1897 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1899 $self->checkin_changed(1);
1903 # returns true if the item was used (or may potentially be used
1904 # in subsequent calls) to capture a hold.
1905 sub attempt_checkin_hold_capture {
1907 my $copy = $self->copy;
1909 # we've been explicitly told not to capture any holds
1910 return 0 if $self->capture eq 'nocapture';
1912 # See if this copy can fulfill any holds
1913 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1914 $self->editor, $copy, $self->editor->requestor );
1917 $logger->debug("circulator: no potential permitted".
1918 "holds found for copy ".$copy->barcode);
1922 if($self->capture ne 'capture') {
1923 # see if this item is in a hold-capture-delay location
1924 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
1925 if($U->is_true($location->hold_verify)) {
1926 $self->bail_on_events(OpenILS::Event->new('HOLD_CAPTURE_DELAYED'));
1931 $self->retarget($retarget);
1933 $logger->info("circulator: found permitted hold ".
1934 $hold->id . " for copy, capturing...");
1936 $hold->current_copy($copy->id);
1937 $hold->capture_time('now');
1939 # prevent DB errors caused by fetching
1940 # holds from storage, and updating through cstore
1941 $hold->clear_fulfillment_time;
1942 $hold->clear_fulfillment_staff;
1943 $hold->clear_fulfillment_lib;
1944 $hold->clear_expire_time;
1945 $hold->clear_cancel_time;
1946 $hold->clear_prev_check_time unless $hold->prev_check_time;
1948 $self->bail_on_events($self->editor->event)
1949 unless $self->editor->update_action_hold_request($hold);
1951 $self->checkin_changed(1);
1953 return 0 if $self->bail_out;
1955 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1957 # This hold was captured in the correct location
1958 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1959 $self->push_events(OpenILS::Event->new('SUCCESS'));
1961 #$self->do_hold_notify($hold->id);
1962 $self->notify_hold($hold->id);
1966 # Hold needs to be picked up elsewhere. Build a hold
1967 # transit and route the item.
1968 $self->checkin_build_hold_transit();
1969 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1970 return 0 if $self->bail_out;
1971 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1974 # make sure we save the copy status
1979 sub do_hold_notify {
1980 my( $self, $holdid ) = @_;
1982 $logger->info("circulator: running delayed hold notify process");
1984 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1985 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1987 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1988 hold_id => $holdid, requestor => $self->editor->requestor);
1990 $logger->debug("circulator: built hold notifier");
1992 if(!$notifier->event) {
1994 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1996 my $stat = $notifier->send_email_notify;
1997 if( $stat == '1' ) {
1998 $logger->info("ciculator: hold notify succeeded for hold $holdid");
2002 $logger->warn("ciculator: * hold notify failed for hold $holdid");
2005 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
2009 sub retarget_holds {
2010 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
2011 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2012 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
2013 # no reason to wait for the return value
2017 sub checkin_build_hold_transit {
2020 my $copy = $self->copy;
2021 my $hold = $self->hold;
2022 my $trans = Fieldmapper::action::hold_transit_copy->new;
2024 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2026 $trans->hold($hold->id);
2027 $trans->source($self->editor->requestor->ws_ou);
2028 $trans->dest($hold->pickup_lib);
2029 $trans->source_send_time("now");
2030 $trans->target_copy($copy->id);
2032 # when the copy gets to its destination, it will recover
2033 # this status - put it onto the holds shelf
2034 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2036 return $self->bail_on_events($self->editor->event)
2037 unless $self->editor->create_action_hold_transit_copy($trans);
2042 sub process_received_transit {
2044 my $copy = $self->copy;
2045 my $copyid = $self->copy->id;
2047 my $status_name = $U->copy_status($copy->status)->name;
2048 $logger->debug("circulator: attempting transit receive on ".
2049 "copy $copyid. Copy status is $status_name");
2051 my $transit = $self->transit;
2053 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2054 # - this item is in-transit to a different location
2056 my $tid = $transit->id;
2057 my $loc = $self->editor->requestor->ws_ou;
2058 my $dest = $transit->dest;
2060 $logger->info("circulator: Fowarding transit on copy which is destined ".
2061 "for a different location. transit=$tid, copy=$copyid, current ".
2062 "location=$loc, destination location=$dest");
2064 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2066 # grab the associated hold object if available
2067 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2068 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2070 return $self->bail_on_events($evt);
2073 # The transit is received, set the receive time
2074 $transit->dest_recv_time('now');
2075 $self->bail_on_events($self->editor->event)
2076 unless $self->editor->update_action_transit_copy($transit);
2078 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2080 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2081 $copy->status( $transit->copy_status );
2082 $self->update_copy();
2083 return if $self->bail_out;
2087 #$self->do_hold_notify($hold_transit->hold);
2088 $self->notify_hold($hold_transit->hold);
2093 OpenILS::Event->new(
2096 payload => { transit => $transit, holdtransit => $hold_transit } ));
2098 return $hold_transit;
2102 sub checkin_handle_circ {
2106 my $circ = $self->circ;
2107 my $copy = $self->copy;
2111 # backdate the circ if necessary
2112 if($self->backdate) {
2113 $self->checkin_handle_backdate;
2114 return if $self->bail_out;
2117 if(!$circ->stop_fines) {
2118 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2119 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2120 $circ->stop_fines_time('now') unless $self->backdate;
2121 $circ->stop_fines_time($self->backdate) if $self->backdate;
2124 # see if there are any fines owed on this circ. if not, close it
2125 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2126 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2128 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2130 # Set the checkin vars since we have the item
2131 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2133 $circ->checkin_staff($self->editor->requestor->id);
2134 $circ->checkin_lib($self->editor->requestor->ws_ou);
2136 my $circ_lib = (ref $self->copy->circ_lib) ?
2137 $self->copy->circ_lib->id : $self->copy->circ_lib;
2138 my $stat = $U->copy_status($self->copy->status)->id;
2140 # If the item is lost/missing and it needs to be sent home, don't
2141 # reshelve the copy, leave it lost/missing so the recipient will know
2142 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2143 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2144 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2147 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2151 return $self->bail_on_events($self->editor->event)
2152 unless $self->editor->update_action_circulation($circ);
2156 sub checkin_handle_backdate {
2159 my $bd = $self->backdate;
2161 # ------------------------------------------------------------------
2162 # clean up the backdate for date comparison
2163 # we want any bills created on or after the backdate
2164 # ------------------------------------------------------------------
2165 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2166 #$bd = "${bd}T23:59:59";
2168 my $bills = $self->editor->search_money_billing(
2170 billing_ts => { '>=' => $bd },
2171 xact => $self->circ->id,
2172 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2176 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2178 for my $bill (@$bills) {
2179 unless( $U->is_true($bill->voided) ) {
2180 $logger->info("backdate voiding bill ".$bill->id);
2182 $bill->void_time('now');
2183 $bill->voider($self->editor->requestor->id);
2184 my $n = $bill->note || "";
2185 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2187 $self->bail_on_events($self->editor->event)
2188 unless $self->editor->update_money_billing($bill);
2196 sub find_patron_from_copy {
2198 my $circs = $self->editor->search_action_circulation(
2199 { target_copy => $self->copy->id, checkin_time => undef });
2200 my $circ = $circs->[0];
2201 return unless $circ;
2202 my $u = $self->editor->retrieve_actor_user($circ->usr)
2203 or return $self->bail_on_events($self->editor->event);
2207 sub check_checkin_copy_status {
2209 my $copy = $self->copy;
2215 my $status = $U->copy_status($copy->status)->id;
2218 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2219 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2220 $status == OILS_COPY_STATUS_IN_PROCESS ||
2221 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2222 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2223 $status == OILS_COPY_STATUS_CATALOGING ||
2224 $status == OILS_COPY_STATUS_RESHELVING );
2226 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2227 if( $status == OILS_COPY_STATUS_LOST );
2229 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2230 if( $status == OILS_COPY_STATUS_MISSING );
2232 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2237 # --------------------------------------------------------------------------
2238 # On checkin, we need to return as many relevant objects as we can
2239 # --------------------------------------------------------------------------
2240 sub checkin_flesh_events {
2243 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2244 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2245 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2249 for my $evt (@{$self->events}) {
2252 $payload->{copy} = $U->unflesh_copy($self->copy);
2253 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2254 $payload->{circ} = $self->circ;
2255 $payload->{transit} = $self->transit;
2256 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2258 # $self->hold may or may not have been replaced with a
2259 # valid hold after processing a cancelled hold
2260 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2262 $evt->{payload} = $payload;
2267 my( $self, $msg ) = @_;
2268 my $bc = ($self->copy) ? $self->copy->barcode :
2271 my $usr = ($self->patron) ? $self->patron->id : "";
2272 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2273 ", recipient=$usr, copy=$bc");
2279 $self->log_me("do_renew()");
2280 $self->is_renewal(1);
2282 # Make sure there is an open circ to renew that is not
2283 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2284 my $circ = $self->editor->search_action_circulation(
2285 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2288 $circ = $self->editor->search_action_circulation(
2290 target_copy => $self->copy->id,
2291 stop_fines => OILS_STOP_FINES_MAX_FINES,
2292 checkin_time => undef
2297 return $self->bail_on_events($self->editor->event) unless $circ;
2299 # A user is not allowed to renew another user's items without permission
2300 unless( $circ->usr eq $self->editor->requestor->id ) {
2301 return $self->bail_on_events($self->editor->events)
2302 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2305 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2306 if $circ->renewal_remaining < 1;
2308 # -----------------------------------------------------------------
2310 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2313 $self->run_renew_permit;
2316 $self->do_checkin();
2317 return if $self->bail_out;
2319 unless( $self->permit_override ) {
2321 return if $self->bail_out;
2322 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2323 $self->remove_event('ITEM_NOT_CATALOGED');
2326 $self->override_events;
2327 return if $self->bail_out;
2330 $self->do_checkout();
2335 my( $self, $evt ) = @_;
2336 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2337 $logger->debug("circulator: removing event from list: $evt");
2338 my @events = @{$self->events};
2339 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2344 my( $self, $evt ) = @_;
2345 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2346 return grep { $_->{textcode} eq $evt } @{$self->events};
2351 sub run_renew_permit {
2356 if(!$self->legacy_script_support) {
2357 my $results = $self->run_indb_circ_test;
2358 unless($self->circ_test_success) {
2359 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2364 my $runner = $self->script_runner;
2366 $runner->load($self->circ_permit_renew);
2367 my $result = $runner->run or
2368 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2369 my $events = $result->{events};
2372 $logger->activity("ciculator: circ_permit_renew for user ".
2373 $self->patron->id." returned events: @$events") if @$events;
2375 $self->push_events(OpenILS::Event->new($_)) for @$events;
2377 $logger->debug("circulator: re-creating script runner to be safe");
2378 #$self->mk_script_runner;