1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
8 use OpenILS::Application::AppUtils;
9 my $U = "OpenILS::Application::AppUtils";
13 my $legacy_script_support = 0;
18 my $conf = OpenSRF::Utils::SettingsClient->new;
19 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
20 my @pfx = ( @pfx2, "scripts" );
22 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
23 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
24 my $d = $conf->config_value( @pfx, 'circ_duration' );
25 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
26 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
27 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
28 my $lb = $conf->config_value( @pfx2, 'script_path' );
30 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
31 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
33 $logger->error( "Missing circ script(s)" )
34 unless( $p and $c and $d and $f and $m and $pr );
36 $scripts{circ_permit_patron} = $p;
37 $scripts{circ_permit_copy} = $c;
38 $scripts{circ_duration} = $d;
39 $scripts{circ_recurring_fines}= $f;
40 $scripts{circ_max_fines} = $m;
41 $scripts{circ_permit_renew} = $pr;
43 $lb = [ $lb ] unless ref($lb);
47 "circulator: Loaded rules scripts for circ: " .
48 "circ permit patron = $p, ".
49 "circ permit copy = $c, ".
50 "circ duration = $d, ".
51 "circ recurring fines = $f, " .
52 "circ max fines = $m, ".
53 "circ renew permit = $pr. ".
55 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
60 __PACKAGE__->register_method(
61 method => "run_method",
62 api_name => "open-ils.circ.checkout.permit",
64 Determines if the given checkout can occur
65 @param authtoken The login session key
66 @param params A trailing hash of named params including
67 barcode : The copy barcode,
68 patron : The patron the checkout is occurring for,
69 renew : true or false - whether or not this is a renewal
70 @return The event that occurred during the permit check.
74 __PACKAGE__->register_method (
75 method => 'run_method',
76 api_name => 'open-ils.circ.checkout.permit.override',
77 signature => q/@see open-ils.circ.checkout.permit/,
81 __PACKAGE__->register_method(
82 method => "run_method",
83 api_name => "open-ils.circ.checkout",
86 @param authtoken The login session key
87 @param params A named hash of params including:
89 barcode If no copy is provided, the copy is retrieved via barcode
90 copyid If no copy or barcode is provide, the copy id will be use
91 patron The patron's id
92 noncat True if this is a circulation for a non-cataloted item
93 noncat_type The non-cataloged type id
94 noncat_circ_lib The location for the noncat circ.
95 precat The item has yet to be cataloged
96 dummy_title The temporary title of the pre-cataloded item
97 dummy_author The temporary authr of the pre-cataloded item
98 Default is the home org of the staff member
99 @return The SUCCESS event on success, any other event depending on the error
102 __PACKAGE__->register_method(
103 method => "run_method",
104 api_name => "open-ils.circ.checkin",
107 Generic super-method for handling all copies
108 @param authtoken The login session key
109 @param params Hash of named parameters including:
110 barcode - The copy barcode
111 force - If true, copies in bad statuses will be checked in and give good statuses
116 __PACKAGE__->register_method(
117 method => "run_method",
118 api_name => "open-ils.circ.checkin.override",
119 signature => q/@see open-ils.circ.checkin/
122 __PACKAGE__->register_method(
123 method => "run_method",
124 api_name => "open-ils.circ.renew.override",
125 signature => q/@see open-ils.circ.renew/,
129 __PACKAGE__->register_method(
130 method => "run_method",
131 api_name => "open-ils.circ.renew",
132 notes => <<" NOTES");
133 PARAMS( authtoken, circ => circ_id );
134 open-ils.circ.renew(login_session, circ_object);
135 Renews the provided circulation. login_session is the requestor of the
136 renewal and if the logged in user is not the same as circ->usr, then
137 the logged in user must have RENEW_CIRC permissions.
140 __PACKAGE__->register_method(
141 method => "run_method",
142 api_name => "open-ils.circ.checkout.full");
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkout.full.override");
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.inspect",
151 Returns the circ matrix test result and, on success, the rule set and matrix test object
158 my( $self, $conn, $auth, $args ) = @_;
159 translate_legacy_args($args);
160 my $api = $self->api_name;
163 OpenILS::Application::Circ::Circulator->new($auth, %$args);
165 return circ_events($circulator) if $circulator->bail_out;
167 # --------------------------------------------------------------------------
168 # Go ahead and load the script runner to make sure we have all
169 # of the objects we need
170 # --------------------------------------------------------------------------
171 $circulator->is_renewal(1) if $api =~ /renew/;
172 $circulator->is_checkin(1) if $api =~ /checkin/;
173 $circulator->check_penalty_on_renew(1) if
174 $circulator->is_renewal and $U->ou_ancestor_setting_value(
175 $circulator->editor->requestor->ws_ou, 'circ.renew.check_penalty', $circulator->editor);
177 if($legacy_script_support and not $circulator->is_checkin) {
178 $circulator->mk_script_runner();
179 $circulator->legacy_script_support(1);
180 $circulator->circ_permit_patron($scripts{circ_permit_patron});
181 $circulator->circ_permit_copy($scripts{circ_permit_copy});
182 $circulator->circ_duration($scripts{circ_duration});
183 $circulator->circ_permit_renew($scripts{circ_permit_renew});
185 $circulator->mk_env();
187 return circ_events($circulator) if $circulator->bail_out;
190 $circulator->override(1) if $api =~ /override/o;
192 if( $api =~ /checkout\.permit/ ) {
193 $circulator->do_permit();
195 } elsif( $api =~ /checkout.full/ ) {
197 $circulator->do_permit();
198 unless( $circulator->bail_out ) {
199 $circulator->events([]);
200 $circulator->do_checkout();
203 } elsif( $api =~ /inspect/ ) {
204 my $data = $circulator->do_inspect();
205 $circulator->editor->rollback;
208 } elsif( $api =~ /checkout/ ) {
209 $circulator->do_checkout();
211 } elsif( $api =~ /checkin/ ) {
212 $circulator->do_checkin();
214 } elsif( $api =~ /renew/ ) {
215 $circulator->is_renewal(1);
216 $circulator->do_renew();
219 if( $circulator->bail_out ) {
222 # make sure no success event accidentally slip in
224 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
227 my @e = @{$circulator->events};
228 push( @ee, $_->{textcode} ) for @e;
229 $logger->info("circulator: bailing out with events: @ee");
231 $circulator->editor->rollback;
234 $circulator->editor->commit;
237 $circulator->script_runner->cleanup if $circulator->script_runner;
239 $conn->respond_complete(circ_events($circulator));
241 unless($circulator->bail_out) {
242 $circulator->do_hold_notify($circulator->notify_hold)
243 if $circulator->notify_hold;
244 $circulator->retarget_holds if $circulator->retarget;
250 my @e = @{$circ->events};
251 # if we have multiple events, SUCCESS should not be one of them;
252 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
253 return (@e == 1) ? $e[0] : \@e;
257 sub translate_legacy_args {
260 if( $$args{barcode} ) {
261 $$args{copy_barcode} = $$args{barcode};
262 delete $$args{barcode};
265 if( $$args{copyid} ) {
266 $$args{copy_id} = $$args{copyid};
267 delete $$args{copyid};
270 if( $$args{patronid} ) {
271 $$args{patron_id} = $$args{patronid};
272 delete $$args{patronid};
275 if( $$args{patron} and !ref($$args{patron}) ) {
276 $$args{patron_id} = $$args{patron};
277 delete $$args{patron};
281 if( $$args{noncat} ) {
282 $$args{is_noncat} = $$args{noncat};
283 delete $$args{noncat};
286 if( $$args{precat} ) {
287 $$args{is_precat} = $$args{request_precat} = $$args{precat};
288 delete $$args{precat};
294 # --------------------------------------------------------------------------
295 # This package actually manages all of the circulation logic
296 # --------------------------------------------------------------------------
297 package OpenILS::Application::Circ::Circulator;
298 use strict; use warnings;
299 use vars q/$AUTOLOAD/;
301 use OpenILS::Utils::Fieldmapper;
302 use OpenSRF::Utils::Cache;
303 use Digest::MD5 qw(md5_hex);
304 use DateTime::Format::ISO8601;
305 use OpenILS::Utils::PermitHold;
306 use OpenSRF::Utils qw/:datetime/;
307 use OpenSRF::Utils::SettingsClient;
308 use OpenILS::Application::Circ::Holds;
309 use OpenILS::Application::Circ::Transit;
310 use OpenSRF::Utils::Logger qw(:logger);
311 use OpenILS::Utils::CStoreEditor qw/:funcs/;
312 use OpenILS::Application::Circ::ScriptBuilder;
313 use OpenILS::Const qw/:const/;
315 my $holdcode = "OpenILS::Application::Circ::Holds";
316 my $transcode = "OpenILS::Application::Circ::Transit";
321 # --------------------------------------------------------------------------
322 # Add a pile of automagic getter/setter methods
323 # --------------------------------------------------------------------------
324 my @AUTOLOAD_FIELDS = qw/
339 check_penalty_on_renew
367 recurring_fines_level
380 cancelled_hold_transit
389 legacy_script_support
399 my $type = ref($self) or die "$self is not an object";
401 my $name = $AUTOLOAD;
404 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
405 $logger->error("circulator: $type: invalid autoload field: $name");
406 die "$type: invalid autoload field: $name\n"
411 *{"${type}::${name}"} = sub {
414 $s->{$name} = $v if defined $v;
418 return $self->$name($data);
423 my( $class, $auth, %args ) = @_;
424 $class = ref($class) || $class;
425 my $self = bless( {}, $class );
429 new_editor(xact => 1, authtoken => $auth) );
431 unless( $self->editor->checkauth ) {
432 $self->bail_on_events($self->editor->event);
436 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
438 $self->$_($args{$_}) for keys %args;
441 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
443 # if this is a renewal, default to desk_renewal
444 $self->desk_renewal(1) unless
445 $self->opac_renewal or $self->phone_renewal;
451 # --------------------------------------------------------------------------
452 # True if we should discontinue processing
453 # --------------------------------------------------------------------------
455 my( $self, $bool ) = @_;
456 if( defined $bool ) {
457 $logger->info("circulator: BAILING OUT") if $bool;
458 $self->{bail_out} = $bool;
460 return $self->{bail_out};
465 my( $self, @evts ) = @_;
468 $logger->info("circulator: pushing event ".$e->{textcode});
469 push( @{$self->events}, $e ) unless
470 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
476 my $key = md5_hex( time() . rand() . "$$" );
477 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
478 return $self->permit_key($key);
481 sub check_permit_key {
483 my $key = $self->permit_key;
484 return 0 unless $key;
485 my $k = "oils_permit_key_$key";
486 my $one = $self->cache_handle->get_cache($k);
487 $self->cache_handle->delete_cache($k);
488 return ($one) ? 1 : 0;
493 my $e = $self->editor;
495 # --------------------------------------------------------------------------
496 # Grab the fleshed copy
497 # --------------------------------------------------------------------------
498 unless($self->is_noncat) {
502 flesh_fields => {acp => ['call_number'], acn => ['record']}
505 $copy = $e->retrieve_asset_copy(
506 [$self->copy_id, $flesh ]) or return $e->event;
508 } elsif( $self->copy_barcode ) {
510 $copy = $e->search_asset_copy(
511 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
516 $self->volume($copy->call_number);
517 $self->title($self->volume->record);
518 $self->copy->call_number($self->volume->id);
519 $self->volume->record($self->title->id);
520 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
521 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
522 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
523 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
526 # We can't renew if there is no copy
527 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
528 if $self->is_renewal;
533 return undef if $self->is_checkin;
535 # --------------------------------------------------------------------------
537 # --------------------------------------------------------------------------
539 if( $self->patron_id ) {
540 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
542 } elsif( $self->patron_barcode ) {
544 my $card = $e->search_actor_card(
545 {barcode => $self->patron_barcode})->[0] or return $e->event;
547 $patron = $e->search_actor_user(
548 {card => $card->id})->[0] or return $e->event;
551 if( my $copy = $self->copy ) {
552 my $circs = $e->search_action_circulation(
553 {target_copy => $copy->id, checkin_time => undef});
555 if( my $circ = $circs->[0] ) {
556 $patron = $e->retrieve_actor_user($circ->usr)
562 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
563 unless $self->patron($patron);
566 # --------------------------------------------------------------------------
567 # This builds the script runner environment and fetches most of the
569 # --------------------------------------------------------------------------
570 sub mk_script_runner {
576 qw/copy copy_barcode copy_id patron
577 patron_id patron_barcode volume title editor/;
579 # Translate our objects into the ScriptBuilder args hash
580 $$args{$_} = $self->$_() for @fields;
582 $args->{ignore_user_status} = 1 if $self->is_checkin;
583 $$args{fetch_patron_by_circ_copy} = 1;
584 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
586 if( my $pco = $self->pending_checkouts ) {
587 $logger->info("circulator: we were given a pending checkouts number of $pco");
588 $$args{patronItemsOut} = $pco;
591 # This fetches most of the objects we need
592 $self->script_runner(
593 OpenILS::Application::Circ::ScriptBuilder->build($args));
595 # Now we translate the ScriptBuilder objects back into self
596 $self->$_($$args{$_}) for @fields;
598 my @evts = @{$args->{_events}} if $args->{_events};
600 $logger->debug("circulator: script builder returned events: @evts") if @evts;
604 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
605 if(!$self->is_noncat and
607 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
611 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
612 return $self->bail_on_events(@e);
617 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
618 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
619 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
620 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
624 # We can't renew if there is no copy
625 return $self->bail_on_events(@evts) if
626 $self->is_renewal and !$self->copy;
628 # Set some circ-specific flags in the script environment
629 my $evt = "environment";
630 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
632 if( $self->is_noncat ) {
633 $self->script_runner->insert("$evt.isNonCat", 1);
634 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
637 if( $self->is_precat ) {
638 $self->script_runner->insert("environment.isPrecat", 1, 1);
641 $self->script_runner->add_path( $_ ) for @$script_libs;
646 # --------------------------------------------------------------------------
647 # Does the circ permit work
648 # --------------------------------------------------------------------------
652 $self->log_me("do_permit()");
654 unless( $self->editor->requestor->id == $self->patron->id ) {
655 return $self->bail_on_events($self->editor->event)
656 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
659 $self->check_captured_holds();
660 $self->do_copy_checks();
661 return if $self->bail_out;
662 $self->run_patron_permit_scripts();
663 $self->run_copy_permit_scripts()
664 unless $self->is_precat or $self->is_noncat;
665 $self->check_item_deposit_events();
666 $self->override_events() unless
667 $self->is_renewal and not $self->check_penalty_on_renew;
668 return if $self->bail_out;
670 if($self->is_precat and not $self->request_precat) {
673 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
674 return $self->bail_out(1) unless $self->is_renewal;
680 payload => $self->mk_permit_key));
683 sub check_item_deposit_events {
685 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED')) if $self->is_deposit;
686 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED')) if $self->is_rental;
689 sub check_captured_holds {
691 my $copy = $self->copy;
692 my $patron = $self->patron;
694 return undef unless $copy;
696 my $s = $U->copy_status($copy->status)->id;
697 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
698 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
700 # Item is on the holds shelf, make sure it's going to the right person
701 my $holds = $self->editor->search_action_hold_request(
704 current_copy => $copy->id ,
705 capture_time => { '!=' => undef },
706 cancel_time => undef,
707 fulfillment_time => undef
713 if( $holds and $$holds[0] ) {
714 return undef if $$holds[0]->usr == $patron->id;
717 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
719 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
725 my $copy = $self->copy;
728 my $stat = $U->copy_status($copy->status)->id;
730 # We cannot check out a copy if it is in-transit
731 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
732 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
735 $self->handle_claims_returned();
736 return if $self->bail_out;
738 # no claims returned circ was found, check if there is any open circ
739 unless( $self->is_renewal ) {
740 my $circs = $self->editor->search_action_circulation(
741 { target_copy => $copy->id, checkin_time => undef }
744 return $self->bail_on_events(
745 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
750 sub send_penalty_request {
752 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
753 $self->penalty_request(
755 'open-ils.penalty.patron_penalty.calculate',
757 authtoken => $self->editor->authtoken,
758 patron => $self->patron } ) );
761 sub gather_penalty_request {
763 return [] unless $self->penalty_request;
764 my $data = $self->penalty_request->recv;
766 throw $data if UNIVERSAL::isa($data,'Error');
767 $data = $data->content;
768 return $data->{fatal_penalties};
770 $logger->error("circulator: penalty request returned no data");
774 my $LEGACY_CIRC_EVENT_MAP = {
775 'actor.usr.barred' => 'PATRON_BARRED',
776 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
777 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
778 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
779 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
780 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
781 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
782 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
783 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
787 # ---------------------------------------------------------------------
788 # This pushes any patron-related events into the list but does not
789 # set bail_out for any events
790 # ---------------------------------------------------------------------
791 sub run_patron_permit_scripts {
793 my $runner = $self->script_runner;
794 my $patronid = $self->patron->id;
798 if(!$self->legacy_script_support) {
800 my $results = $self->run_indb_circ_test;
801 unless($self->circ_test_success) {
802 push(@allevents, OpenILS::Event->new(
803 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
808 $self->send_penalty_request() unless
809 $self->is_renewal and not $self->check_penalty_on_renew;
812 # --------------------------------------------------------------------- # Now run the patron permit script
813 # ---------------------------------------------------------------------
814 $runner->load($self->circ_permit_patron);
815 my $result = $runner->run or
816 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
818 my $patron_events = $result->{events};
820 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
821 [] : $self->gather_penalty_request();
823 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
826 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
828 $self->push_events(@allevents);
831 sub run_indb_circ_test {
833 return $self->matrix_test_result if $self->matrix_test_result;
835 my $dbfunc = ($self->is_renewal) ?
836 'action.item_user_renew_test' : 'action.item_user_circ_test';
838 my $results = ($self->matrix_test_result) ?
839 $self->matrix_test_result :
840 $self->editor->json_query(
843 $self->editor->requestor->ws_ou,
844 ($self->is_precat) ? undef : $self->copy->id,
850 $self->circ_test_success($U->is_true($results->[0]->{success}));
851 if($self->circ_test_success) {
852 $self->circ_matrix_test(
853 $self->editor->retrieve_config_circ_matrix_test(
854 $results->[0]->{matchpoint}
859 if($self->circ_test_success) {
860 $self->circ_matrix_ruleset(
861 $self->editor->retrieve_config_circ_matrix_ruleset([
862 $results->[0]->{matchpoint},
865 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
873 return $self->matrix_test_result($results);
878 $self->run_indb_circ_test;
881 circ_test_success => $self->circ_test_success,
882 failure_events => [],
886 unless($self->circ_test_success) {
887 push(@{$results->{failure_codes}},
888 $_->{fail_part}) for @{$self->matrix_test_result};
889 push(@{$results->{failure_events}},
890 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
891 for @{$self->matrix_test_result};
895 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
896 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
897 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
899 my $policy = $self->get_circ_policy(
900 $duration_rule, $recurring_fine_rule, $max_fine_rule);
902 $$results{$_} = $$policy{$_} for keys %$policy;
906 # ---------------------------------------------------------------------
907 # Loads the circ policy info for duration, recurring fine, and max
908 # fine based on the current copy
909 # ---------------------------------------------------------------------
910 sub get_circ_policy {
911 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
914 duration_rule => $duration_rule->name,
915 recurring_fine_rule => $recurring_fine_rule->name,
916 max_fine_rule => $max_fine_rule->name,
917 max_fine => $self->get_max_fine_amount($max_fine_rule),
918 fine_interval => $recurring_fine_rule->recurance_interval,
919 renewal_remaining => $duration_rule->max_renewals
922 $policy->{duration} = $duration_rule->shrt
923 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
924 $policy->{duration} = $duration_rule->normal
925 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
926 $policy->{duration} = $duration_rule->extended
927 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
929 $policy->{recurring_fine} = $recurring_fine_rule->low
930 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
931 $policy->{recurring_fine} = $recurring_fine_rule->normal
932 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
933 $policy->{recurring_fine} = $recurring_fine_rule->high
934 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
939 sub get_max_fine_amount {
941 my $max_fine_rule = shift;
942 my $max_amount = $max_fine_rule->amount;
944 # if is_percent is true then the max->amount is
945 # use as a percentage of the copy price
946 if ($U->is_true($max_fine_rule->is_percent)) {
948 my $ol = ($self->is_precat) ?
949 $self->editor->requestor->ws_ou : $self->volume->owning_lib;
951 my $default_price = $U->ou_ancestor_setting_value(
952 $ol, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
953 my $charge_on_0 = $U->ou_ancestor_setting_value(
954 $ol, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
956 # Find the most appropriate "price" -- same definition as the
957 # LOST price. See OpenILS::Circ::new_set_circ_lost
958 $max_amount = $self->copy->price;
959 $max_amount = $default_price unless defined $max_amount;
960 $max_amount = 0 if $max_amount < 0;
961 $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
963 $max_amount *= $max_fine_rule->amount / 100;
971 sub run_copy_permit_scripts {
973 my $copy = $self->copy || return;
974 my $runner = $self->script_runner;
978 if(!$self->legacy_script_support) {
979 my $results = $self->run_indb_circ_test;
980 unless($self->circ_test_success) {
981 push(@allevents, OpenILS::Event->new(
982 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
986 # ---------------------------------------------------------------------
987 # Capture all of the copy permit events
988 # ---------------------------------------------------------------------
989 $runner->load($self->circ_permit_copy);
990 my $result = $runner->run or
991 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
992 my $copy_events = $result->{events};
994 # ---------------------------------------------------------------------
995 # Now collect all of the events together
996 # ---------------------------------------------------------------------
997 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1000 # See if this copy has an alert message
1001 my $ae = $self->check_copy_alert();
1002 push( @allevents, $ae ) if $ae;
1004 # uniquify the events
1005 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1006 @allevents = values %hash;
1009 $_->{payload} = $copy if
1010 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1013 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1015 $self->push_events(@allevents);
1019 sub check_copy_alert {
1021 return undef if $self->is_renewal;
1022 return OpenILS::Event->new(
1023 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1024 if $self->copy and $self->copy->alert_message;
1030 # --------------------------------------------------------------------------
1031 # If the call is overriding and has permissions to override every collected
1032 # event, the are cleared. Any event that the caller does not have
1033 # permission to override, will be left in the event list and bail_out will
1035 # XXX We need code in here to cancel any holds/transits on copies
1036 # that are being force-checked out
1037 # --------------------------------------------------------------------------
1038 sub override_events {
1040 my @events = @{$self->events};
1041 return unless @events;
1043 if(!$self->override) {
1044 return $self->bail_out(1)
1045 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1050 for my $e (@events) {
1051 my $tc = $e->{textcode};
1052 next if $tc eq 'SUCCESS';
1053 my $ov = "$tc.override";
1054 $logger->info("circulator: attempting to override event: $ov");
1056 return $self->bail_on_events($self->editor->event)
1057 unless( $self->editor->allowed($ov) );
1062 # --------------------------------------------------------------------------
1063 # If there is an open claimsreturn circ on the requested copy, close the
1064 # circ if overriding, otherwise bail out
1065 # --------------------------------------------------------------------------
1066 sub handle_claims_returned {
1068 my $copy = $self->copy;
1070 my $CR = $self->editor->search_action_circulation(
1072 target_copy => $copy->id,
1073 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1074 checkin_time => undef,
1078 return unless ($CR = $CR->[0]);
1082 # - If the caller has set the override flag, we will check the item in
1083 if($self->override) {
1085 $CR->checkin_time('now');
1086 $CR->checkin_lib($self->editor->requestor->ws_ou);
1087 $CR->checkin_staff($self->editor->requestor->id);
1089 $evt = $self->editor->event
1090 unless $self->editor->update_action_circulation($CR);
1093 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1096 $self->bail_on_events($evt) if $evt;
1101 # --------------------------------------------------------------------------
1102 # This performs the checkout
1103 # --------------------------------------------------------------------------
1107 $self->log_me("do_checkout()");
1109 # make sure perms are good if this isn't a renewal
1110 unless( $self->is_renewal ) {
1111 return $self->bail_on_events($self->editor->event)
1112 unless( $self->editor->allowed('COPY_CHECKOUT') );
1115 # verify the permit key
1116 unless( $self->check_permit_key ) {
1117 if( $self->permit_override ) {
1118 return $self->bail_on_events($self->editor->event)
1119 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1121 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1125 # if this is a non-cataloged circ, build the circ and finish
1126 if( $self->is_noncat ) {
1127 $self->checkout_noncat;
1129 OpenILS::Event->new('SUCCESS',
1130 payload => { noncat_circ => $self->circ }));
1134 if( $self->is_precat ) {
1135 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1136 $self->make_precat_copy;
1137 return if $self->bail_out;
1139 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1140 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1143 $self->do_copy_checks;
1144 return if $self->bail_out;
1146 $self->run_checkout_scripts();
1147 return if $self->bail_out;
1149 $self->build_checkout_circ_object();
1150 return if $self->bail_out;
1152 $self->apply_modified_due_date();
1153 return if $self->bail_out;
1155 return $self->bail_on_events($self->editor->event)
1156 unless $self->editor->create_action_circulation($self->circ);
1158 # refresh the circ to force local time zone for now
1159 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1161 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1163 return if $self->bail_out;
1165 $self->apply_deposit_fee();
1166 return if $self->bail_out;
1168 $self->handle_checkout_holds();
1169 return if $self->bail_out;
1171 # ------------------------------------------------------------------------------
1172 # Update the patron penalty info in the DB. Run it for permit-overrides or
1173 # renewals since both of those cases do not require the penalty server to
1174 # run during the permit phase of the checkout
1175 # ------------------------------------------------------------------------------
1176 if( $self->permit_override or $self->is_renewal ) {
1177 $U->update_patron_penalties(
1178 authtoken => $self->editor->authtoken,
1179 patron => $self->patron,
1184 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1186 OpenILS::Event->new('SUCCESS',
1188 copy => $U->unflesh_copy($self->copy),
1189 circ => $self->circ,
1191 holds_fulfilled => $self->fulfilled_holds,
1192 deposit_billing => $self->deposit_billing,
1193 rental_billing => $self->rental_billing
1199 sub apply_deposit_fee {
1201 my $copy = $self->copy;
1202 return unless $self->is_deposit or $self->is_rental;
1204 my $bill = Fieldmapper::money::billing->new;
1205 my $amount = $copy->deposit_amount;
1208 if($self->is_deposit) {
1209 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1210 $self->deposit_billing($bill);
1212 $billing_type = OILS_BILLING_TYPE_RENTAL;
1213 $self->rental_billing($bill);
1216 $bill->xact($self->circ->id);
1217 $bill->amount($amount);
1218 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1219 $bill->billing_type($billing_type);
1220 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1222 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1227 my $copy = $self->copy;
1229 my $stat = $copy->status if ref $copy->status;
1230 my $loc = $copy->location if ref $copy->location;
1231 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1233 $copy->status($stat->id) if $stat;
1234 $copy->location($loc->id) if $loc;
1235 $copy->circ_lib($circ_lib->id) if $circ_lib;
1236 $copy->editor($self->editor->requestor->id);
1237 $copy->edit_date('now');
1238 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1240 return $self->bail_on_events($self->editor->event)
1241 unless $self->editor->update_asset_copy($self->copy);
1243 $copy->status($U->copy_status($copy->status));
1244 $copy->location($loc) if $loc;
1245 $copy->circ_lib($circ_lib) if $circ_lib;
1249 sub bail_on_events {
1250 my( $self, @evts ) = @_;
1251 $self->push_events(@evts);
1255 sub handle_checkout_holds {
1258 my $copy = $self->copy;
1259 my $patron = $self->patron;
1261 my $holds = $self->editor->search_action_hold_request(
1263 current_copy => $copy->id ,
1264 cancel_time => undef,
1265 fulfillment_time => undef
1271 # XXX We should only fulfill one hold here...
1272 # XXX If a hold was transited to the user who is checking out
1273 # the item, we need to make sure that hold is what's grabbed
1276 # for now, just sort by id to get what should be the oldest hold
1277 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1278 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1279 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1282 my $hold = $myholds[0];
1284 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1286 # if the hold was never officially captured, capture it.
1287 $hold->capture_time('now') unless $hold->capture_time;
1289 # just make sure it's set correctly
1290 $hold->current_copy($copy->id);
1292 $hold->fulfillment_time('now');
1293 $hold->fulfillment_staff($self->editor->requestor->id);
1294 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1296 return $self->bail_on_events($self->editor->event)
1297 unless $self->editor->update_action_hold_request($hold);
1299 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1301 push( @fulfilled, $hold->id );
1304 # If there are any holds placed for other users that point to this copy,
1305 # then we need to un-target those holds so the targeter can pick a new copy
1308 $logger->info("circulator: un-targeting hold ".$_->id.
1309 " because copy ".$copy->id." is getting checked out");
1311 # - make the targeter process this hold at next run
1312 $_->clear_prev_check_time;
1314 # - clear out the targetted copy
1315 $_->clear_current_copy;
1316 $_->clear_capture_time;
1318 return $self->bail_on_event($self->editor->event)
1319 unless $self->editor->update_action_hold_request($_);
1323 $self->fulfilled_holds(\@fulfilled);
1328 sub run_checkout_scripts {
1332 my $runner = $self->script_runner;
1341 if(!$self->legacy_script_support) {
1342 $self->run_indb_circ_test();
1343 $duration = $self->circ_matrix_ruleset->duration_rule;
1344 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1345 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1349 $runner->load($self->circ_duration);
1351 my $result = $runner->run or
1352 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1354 $duration_name = $result->{durationRule};
1355 $recurring_name = $result->{recurringFinesRule};
1356 $max_fine_name = $result->{maxFine};
1359 $duration_name = $duration->name if $duration;
1360 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1363 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1364 return $self->bail_on_events($evt) if $evt;
1366 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1367 return $self->bail_on_events($evt) if $evt;
1369 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1370 return $self->bail_on_events($evt) if $evt;
1375 # The item circulates with an unlimited duration
1381 $self->duration_rule($duration);
1382 $self->recurring_fines_rule($recurring);
1383 $self->max_fine_rule($max_fine);
1387 sub build_checkout_circ_object {
1390 my $circ = Fieldmapper::action::circulation->new;
1391 my $duration = $self->duration_rule;
1392 my $max = $self->max_fine_rule;
1393 my $recurring = $self->recurring_fines_rule;
1394 my $copy = $self->copy;
1395 my $patron = $self->patron;
1399 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1401 my $dname = $duration->name;
1402 my $mname = $max->name;
1403 my $rname = $recurring->name;
1405 $logger->debug("circulator: building circulation ".
1406 "with duration=$dname, maxfine=$mname, recurring=$rname");
1408 $circ->duration($policy->{duration});
1409 $circ->recuring_fine($policy->{recurring_fine});
1410 $circ->duration_rule($duration->name);
1411 $circ->recuring_fine_rule($recurring->name);
1412 $circ->max_fine_rule($max->name);
1413 $circ->max_fine($policy->{max_fine});
1414 $circ->fine_interval($recurring->recurance_interval);
1415 $circ->renewal_remaining($duration->max_renewals);
1419 $logger->info("circulator: copy found with an unlimited circ duration");
1420 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1421 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1422 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1423 $circ->renewal_remaining(0);
1426 $circ->target_copy( $copy->id );
1427 $circ->usr( $patron->id );
1428 $circ->circ_lib( $self->circ_lib );
1430 if( $self->is_renewal ) {
1431 $circ->opac_renewal('t') if $self->opac_renewal;
1432 $circ->phone_renewal('t') if $self->phone_renewal;
1433 $circ->desk_renewal('t') if $self->desk_renewal;
1434 $circ->renewal_remaining($self->renewal_remaining);
1435 $circ->circ_staff($self->editor->requestor->id);
1439 # if the user provided an overiding checkout time,
1440 # (e.g. the checkout really happened several hours ago), then
1441 # we apply that here. Does this need a perm??
1442 $circ->xact_start(clense_ISO8601($self->checkout_time))
1443 if $self->checkout_time;
1445 # if a patron is renewing, 'requestor' will be the patron
1446 $circ->circ_staff($self->editor->requestor->id);
1447 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1453 sub apply_modified_due_date {
1455 my $circ = $self->circ;
1456 my $copy = $self->copy;
1458 if( $self->due_date ) {
1460 return $self->bail_on_events($self->editor->event)
1461 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1463 $circ->due_date(clense_ISO8601($self->due_date));
1467 # if the due_date lands on a day when the location is closed
1468 return unless $copy and $circ->due_date;
1470 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1472 # due-date overlap should be determined by the location the item
1473 # is checked out from, not the owning or circ lib of the item
1474 my $org = $self->editor->requestor->ws_ou;
1476 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1477 " with an item due date of ".$circ->due_date );
1479 my $dateinfo = $U->storagereq(
1480 'open-ils.storage.actor.org_unit.closed_date.overlap',
1481 $org, $circ->due_date );
1484 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1485 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1487 # XXX make the behavior more dynamic
1488 # for now, we just push the due date to after the close date
1489 $circ->due_date($dateinfo->{end});
1496 sub create_due_date {
1497 my( $self, $duration ) = @_;
1498 # if there is a raw time component (e.g. from postgres),
1499 # turn it into an interval that interval_to_seconds can parse
1500 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1501 my ($sec,$min,$hour,$mday,$mon,$year) =
1502 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1503 $year += 1900; $mon += 1;
1504 my $due_date = sprintf(
1505 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1506 $year, $mon, $mday, $hour, $min, $sec);
1512 sub make_precat_copy {
1514 my $copy = $self->copy;
1517 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1519 $copy->editor($self->editor->requestor->id);
1520 $copy->edit_date('now');
1521 $copy->dummy_title($self->dummy_title);
1522 $copy->dummy_author($self->dummy_author);
1524 $self->update_copy();
1528 $logger->info("circulator: Creating a new precataloged ".
1529 "copy in checkout with barcode " . $self->copy_barcode);
1531 $copy = Fieldmapper::asset::copy->new;
1532 $copy->circ_lib($self->circ_lib);
1533 $copy->creator($self->editor->requestor->id);
1534 $copy->editor($self->editor->requestor->id);
1535 $copy->barcode($self->copy_barcode);
1536 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1537 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1538 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1540 $copy->dummy_title($self->dummy_title || "");
1541 $copy->dummy_author($self->dummy_author || "");
1543 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1545 $self->push_events($self->editor->event);
1549 # this is a little bit of a hack, but we need to
1550 # get the copy into the script runner
1551 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1555 sub checkout_noncat {
1561 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1562 my $count = $self->noncat_count || 1;
1563 my $cotime = clense_ISO8601($self->checkout_time) || "";
1565 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1569 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1570 $self->editor->requestor->id,
1578 $self->push_events($evt);
1589 $self->log_me("do_checkin()");
1592 return $self->bail_on_events(
1593 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1596 if( $self->checkin_check_holds_shelf() ) {
1597 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1598 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1599 $self->checkin_flesh_events;
1603 unless( $self->is_renewal ) {
1604 return $self->bail_on_events($self->editor->event)
1605 unless $self->editor->allowed('COPY_CHECKIN');
1608 $self->push_events($self->check_copy_alert());
1609 $self->push_events($self->check_checkin_copy_status());
1611 # the renew code will have already found our circulation object
1612 unless( $self->is_renewal and $self->circ ) {
1613 my $circs = $self->editor->search_action_circulation(
1614 { target_copy => $self->copy->id, checkin_time => undef });
1615 $self->circ($$circs[0]);
1617 # for now, just warn if there are multiple open circs on a copy
1618 $logger->warn("circulator: we have ".scalar(@$circs).
1619 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1622 # if the circ is marked as 'claims returned', add the event to the list
1623 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1624 if ($self->circ and $self->circ->stop_fines
1625 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1627 $self->check_circ_deposit();
1629 # handle the overridable events
1630 $self->override_events unless $self->is_renewal;
1631 return if $self->bail_out;
1635 $self->editor->search_action_transit_copy(
1636 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1640 $self->checkin_handle_circ;
1641 return if $self->bail_out;
1642 $self->checkin_changed(1);
1644 } elsif( $self->transit ) {
1645 my $hold_transit = $self->process_received_transit;
1646 $self->checkin_changed(1);
1648 if( $self->bail_out ) {
1649 $self->checkin_flesh_events;
1653 if( my $e = $self->check_checkin_copy_status() ) {
1654 # If the original copy status is special, alert the caller
1655 my $ev = $self->events;
1656 $self->events([$e]);
1657 $self->override_events;
1658 return if $self->bail_out;
1662 if( $hold_transit or
1663 $U->copy_status($self->copy->status)->id
1664 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1667 if( $hold_transit ) {
1668 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1670 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1675 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1677 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1678 $self->reshelve_copy(1);
1679 $self->cancelled_hold_transit(1);
1680 $self->notify_hold(0); # don't notify for cancelled holds
1681 return if $self->bail_out;
1685 # hold transited to correct location
1686 $self->checkin_flesh_events;
1691 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1693 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1694 " that is in-transit, but there is no transit.. repairing");
1695 $self->reshelve_copy(1);
1696 return if $self->bail_out;
1699 if( $self->is_renewal ) {
1700 $self->push_events(OpenILS::Event->new('SUCCESS'));
1704 # ------------------------------------------------------------------------------
1705 # Circulations and transits are now closed where necessary. Now go on to see if
1706 # this copy can fulfill a hold or needs to be routed to a different location
1707 # ------------------------------------------------------------------------------
1709 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1710 return if $self->bail_out;
1712 } else { # not needed for a hold
1714 my $circ_lib = (ref $self->copy->circ_lib) ?
1715 $self->copy->circ_lib->id : $self->copy->circ_lib;
1717 if( $self->remote_hold ) {
1718 $circ_lib = $self->remote_hold->pickup_lib;
1719 $logger->warn("circulator: Copy ".$self->copy->barcode.
1720 " is on a remote hold's shelf, sending to $circ_lib");
1723 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1725 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1727 $self->checkin_handle_precat();
1728 return if $self->bail_out;
1732 my $bc = $self->copy->barcode;
1733 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1734 $self->checkin_build_copy_transit($circ_lib);
1735 return if $self->bail_out;
1736 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1740 $self->reshelve_copy;
1741 return if $self->bail_out;
1743 unless($self->checkin_changed) {
1745 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1746 my $stat = $U->copy_status($self->copy->status)->id;
1748 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1749 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1750 $self->bail_out(1); # no need to commit anything
1754 $self->push_events(OpenILS::Event->new('SUCCESS'))
1755 unless @{$self->events};
1759 # ------------------------------------------------------------------------------
1760 # Update the patron penalty info in the DB
1761 # ------------------------------------------------------------------------------
1762 $U->update_patron_penalties(
1763 authtoken => $self->editor->authtoken,
1764 patron => $self->patron,
1765 background => 1 ) if $self->is_checkin;
1767 $self->checkin_flesh_events;
1771 # if a deposit was payed for this item, push the event
1772 sub check_circ_deposit {
1774 return unless $self->circ;
1775 my $deposit = $self->editor->search_money_billing(
1776 { billing_type => OILS_BILLING_TYPE_DEPOSIT,
1777 xact => $self->circ->id,
1779 }, {idlist => 1})->[0];
1781 $self->push_events(OpenILS::Event->new(
1782 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1787 my $force = $self->force || shift;
1788 my $copy = $self->copy;
1790 my $stat = $U->copy_status($copy->status)->id;
1793 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1794 $stat != OILS_COPY_STATUS_CATALOGING and
1795 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1796 $stat != OILS_COPY_STATUS_RESHELVING )) {
1798 $copy->status( OILS_COPY_STATUS_RESHELVING );
1800 $self->checkin_changed(1);
1805 # Returns true if the item is at the current location
1806 # because it was transited there for a hold and the
1807 # hold has not been fulfilled
1808 sub checkin_check_holds_shelf {
1810 return 0 unless $self->copy;
1813 $U->copy_status($self->copy->status)->id ==
1814 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1816 # find the hold that put us on the holds shelf
1817 my $holds = $self->editor->search_action_hold_request(
1819 current_copy => $self->copy->id,
1820 capture_time => { '!=' => undef },
1821 fulfillment_time => undef,
1822 cancel_time => undef,
1827 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1828 $self->reshelve_copy(1);
1832 my $hold = $$holds[0];
1834 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1835 $hold->id. "] for copy ".$self->copy->barcode);
1837 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1838 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1842 $logger->info("circulator: hold is not for here..");
1843 $self->remote_hold($hold);
1848 sub checkin_handle_precat {
1850 my $copy = $self->copy;
1852 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1853 $copy->status(OILS_COPY_STATUS_CATALOGING);
1854 $self->update_copy();
1855 $self->checkin_changed(1);
1856 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1861 sub checkin_build_copy_transit {
1864 my $copy = $self->copy;
1865 my $transit = Fieldmapper::action::transit_copy->new;
1867 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1868 $logger->info("circulator: transiting copy to $dest");
1870 $transit->source($self->editor->requestor->ws_ou);
1871 $transit->dest($dest);
1872 $transit->target_copy($copy->id);
1873 $transit->source_send_time('now');
1874 $transit->copy_status( $U->copy_status($copy->status)->id );
1876 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1878 return $self->bail_on_events($self->editor->event)
1879 unless $self->editor->create_action_transit_copy($transit);
1881 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1883 $self->checkin_changed(1);
1887 sub attempt_checkin_hold_capture {
1889 my $copy = $self->copy;
1891 # See if this copy can fulfill any holds
1892 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1893 $self->editor, $copy, $self->editor->requestor );
1896 $logger->debug("circulator: no potential permitted".
1897 "holds found for copy ".$copy->barcode);
1901 $self->retarget($retarget);
1903 $logger->info("circulator: found permitted hold ".
1904 $hold->id . " for copy, capturing...");
1906 $hold->current_copy($copy->id);
1907 $hold->capture_time('now');
1909 # prevent DB errors caused by fetching
1910 # holds from storage, and updating through cstore
1911 $hold->clear_fulfillment_time;
1912 $hold->clear_fulfillment_staff;
1913 $hold->clear_fulfillment_lib;
1914 $hold->clear_expire_time;
1915 $hold->clear_cancel_time;
1916 $hold->clear_prev_check_time unless $hold->prev_check_time;
1918 $self->bail_on_events($self->editor->event)
1919 unless $self->editor->update_action_hold_request($hold);
1921 $self->checkin_changed(1);
1923 return 1 if $self->bail_out;
1925 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1927 # This hold was captured in the correct location
1928 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1929 $self->push_events(OpenILS::Event->new('SUCCESS'));
1931 #$self->do_hold_notify($hold->id);
1932 $self->notify_hold($hold->id);
1936 # Hold needs to be picked up elsewhere. Build a hold
1937 # transit and route the item.
1938 $self->checkin_build_hold_transit();
1939 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1940 return 1 if $self->bail_out;
1942 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1945 # make sure we save the copy status
1950 sub do_hold_notify {
1951 my( $self, $holdid ) = @_;
1953 $logger->info("circulator: running delayed hold notify process");
1955 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1956 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1958 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1959 hold_id => $holdid, requestor => $self->editor->requestor);
1961 $logger->debug("circulator: built hold notifier");
1963 if(!$notifier->event) {
1965 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1967 my $stat = $notifier->send_email_notify;
1968 if( $stat == '1' ) {
1969 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1973 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1976 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1980 sub retarget_holds {
1981 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1982 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1983 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1984 # no reason to wait for the return value
1988 sub checkin_build_hold_transit {
1991 my $copy = $self->copy;
1992 my $hold = $self->hold;
1993 my $trans = Fieldmapper::action::hold_transit_copy->new;
1995 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1997 $trans->hold($hold->id);
1998 $trans->source($self->editor->requestor->ws_ou);
1999 $trans->dest($hold->pickup_lib);
2000 $trans->source_send_time("now");
2001 $trans->target_copy($copy->id);
2003 # when the copy gets to its destination, it will recover
2004 # this status - put it onto the holds shelf
2005 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2007 return $self->bail_on_events($self->editor->event)
2008 unless $self->editor->create_action_hold_transit_copy($trans);
2013 sub process_received_transit {
2015 my $copy = $self->copy;
2016 my $copyid = $self->copy->id;
2018 my $status_name = $U->copy_status($copy->status)->name;
2019 $logger->debug("circulator: attempting transit receive on ".
2020 "copy $copyid. Copy status is $status_name");
2022 my $transit = $self->transit;
2024 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2025 # - this item is in-transit to a different location
2027 my $tid = $transit->id;
2028 my $loc = $self->editor->requestor->ws_ou;
2029 my $dest = $transit->dest;
2031 $logger->info("circulator: Fowarding transit on copy which is destined ".
2032 "for a different location. transit=$tid, copy=$copyid, current ".
2033 "location=$loc, destination location=$dest");
2035 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2037 # grab the associated hold object if available
2038 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2039 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2041 return $self->bail_on_events($evt);
2044 # The transit is received, set the receive time
2045 $transit->dest_recv_time('now');
2046 $self->bail_on_events($self->editor->event)
2047 unless $self->editor->update_action_transit_copy($transit);
2049 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2051 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2052 $copy->status( $transit->copy_status );
2053 $self->update_copy();
2054 return if $self->bail_out;
2058 #$self->do_hold_notify($hold_transit->hold);
2059 $self->notify_hold($hold_transit->hold);
2064 OpenILS::Event->new(
2067 payload => { transit => $transit, holdtransit => $hold_transit } ));
2069 return $hold_transit;
2073 sub checkin_handle_circ {
2077 my $circ = $self->circ;
2078 my $copy = $self->copy;
2082 # backdate the circ if necessary
2083 if($self->backdate) {
2084 $self->checkin_handle_backdate;
2085 return if $self->bail_out;
2088 if(!$circ->stop_fines) {
2089 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2090 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2091 $circ->stop_fines_time('now') unless $self->backdate;
2092 $circ->stop_fines_time($self->backdate) if $self->backdate;
2095 # see if there are any fines owed on this circ. if not, close it
2096 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2097 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2099 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2101 # Set the checkin vars since we have the item
2102 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2104 $circ->checkin_staff($self->editor->requestor->id);
2105 $circ->checkin_lib($self->editor->requestor->ws_ou);
2107 my $circ_lib = (ref $self->copy->circ_lib) ?
2108 $self->copy->circ_lib->id : $self->copy->circ_lib;
2109 my $stat = $U->copy_status($self->copy->status)->id;
2111 # If the item is lost/missing and it needs to be sent home, don't
2112 # reshelve the copy, leave it lost/missing so the recipient will know
2113 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2114 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2115 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2118 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2122 return $self->bail_on_events($self->editor->event)
2123 unless $self->editor->update_action_circulation($circ);
2127 sub checkin_handle_backdate {
2130 my $bd = $self->backdate;
2132 # ------------------------------------------------------------------
2133 # clean up the backdate for date comparison
2134 # we want any bills created on or after the backdate
2135 # ------------------------------------------------------------------
2136 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2137 #$bd = "${bd}T23:59:59";
2139 my $bills = $self->editor->search_money_billing(
2141 billing_ts => { '>=' => $bd },
2142 xact => $self->circ->id,
2143 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2147 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2149 for my $bill (@$bills) {
2150 unless( $U->is_true($bill->voided) ) {
2151 $logger->info("backdate voiding bill ".$bill->id);
2153 $bill->void_time('now');
2154 $bill->voider($self->editor->requestor->id);
2155 my $n = $bill->note || "";
2156 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2158 $self->bail_on_events($self->editor->event)
2159 unless $self->editor->update_money_billing($bill);
2167 sub find_patron_from_copy {
2169 my $circs = $self->editor->search_action_circulation(
2170 { target_copy => $self->copy->id, checkin_time => undef });
2171 my $circ = $circs->[0];
2172 return unless $circ;
2173 my $u = $self->editor->retrieve_actor_user($circ->usr)
2174 or return $self->bail_on_events($self->editor->event);
2178 sub check_checkin_copy_status {
2180 my $copy = $self->copy;
2186 my $status = $U->copy_status($copy->status)->id;
2189 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2190 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2191 $status == OILS_COPY_STATUS_IN_PROCESS ||
2192 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2193 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2194 $status == OILS_COPY_STATUS_CATALOGING ||
2195 $status == OILS_COPY_STATUS_RESHELVING );
2197 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2198 if( $status == OILS_COPY_STATUS_LOST );
2200 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2201 if( $status == OILS_COPY_STATUS_MISSING );
2203 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2208 # --------------------------------------------------------------------------
2209 # On checkin, we need to return as many relevant objects as we can
2210 # --------------------------------------------------------------------------
2211 sub checkin_flesh_events {
2214 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2215 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2216 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2220 for my $evt (@{$self->events}) {
2223 $payload->{copy} = $U->unflesh_copy($self->copy);
2224 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2225 $payload->{circ} = $self->circ;
2226 $payload->{transit} = $self->transit;
2227 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2229 # $self->hold may or may not have been replaced with a
2230 # valid hold after processing a cancelled hold
2231 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2233 $evt->{payload} = $payload;
2238 my( $self, $msg ) = @_;
2239 my $bc = ($self->copy) ? $self->copy->barcode :
2242 my $usr = ($self->patron) ? $self->patron->id : "";
2243 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2244 ", recipient=$usr, copy=$bc");
2250 $self->log_me("do_renew()");
2251 $self->is_renewal(1);
2253 # Make sure there is an open circ to renew that is not
2254 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2255 my $circ = $self->editor->search_action_circulation(
2256 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2259 $circ = $self->editor->search_action_circulation(
2261 target_copy => $self->copy->id,
2262 stop_fines => OILS_STOP_FINES_MAX_FINES,
2263 checkin_time => undef
2268 return $self->bail_on_events($self->editor->event) unless $circ;
2270 # A user is not allowed to renew another user's items without permission
2271 unless( $circ->usr eq $self->editor->requestor->id ) {
2272 return $self->bail_on_events($self->editor->events)
2273 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2276 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2277 if $circ->renewal_remaining < 1;
2279 # -----------------------------------------------------------------
2281 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2284 $self->run_renew_permit;
2287 $self->do_checkin();
2288 return if $self->bail_out;
2290 unless( $self->permit_override ) {
2292 return if $self->bail_out;
2293 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2294 $self->remove_event('ITEM_NOT_CATALOGED');
2297 $self->override_events;
2298 return if $self->bail_out;
2301 $self->do_checkout();
2306 my( $self, $evt ) = @_;
2307 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2308 $logger->debug("circulator: removing event from list: $evt");
2309 my @events = @{$self->events};
2310 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2315 my( $self, $evt ) = @_;
2316 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2317 return grep { $_->{textcode} eq $evt } @{$self->events};
2322 sub run_renew_permit {
2327 if(!$self->legacy_script_support) {
2328 my $results = $self->run_indb_circ_test;
2329 unless($self->circ_test_success) {
2330 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2335 my $runner = $self->script_runner;
2337 $runner->load($self->circ_permit_renew);
2338 my $result = $runner->run or
2339 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2340 my $events = $result->{events};
2343 $logger->activity("ciculator: circ_permit_renew for user ".
2344 $self->patron->id." returned events: @$events") if @$events;
2346 $self->push_events(OpenILS::Event->new($_)) for @$events;
2348 $logger->debug("circulator: re-creating script runner to be safe");
2349 #$self->mk_script_runner;