1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
8 use OpenILS::Application::AppUtils;
9 my $U = "OpenILS::Application::AppUtils";
13 my $legacy_script_support = 0;
18 my $conf = OpenSRF::Utils::SettingsClient->new;
19 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
20 my @pfx = ( @pfx2, "scripts" );
22 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
23 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
24 my $d = $conf->config_value( @pfx, 'circ_duration' );
25 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
26 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
27 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
28 my $lb = $conf->config_value( @pfx2, 'script_path' );
30 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
31 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
33 $logger->error( "Missing circ script(s)" )
34 unless( $p and $c and $d and $f and $m and $pr );
36 $scripts{circ_permit_patron} = $p;
37 $scripts{circ_permit_copy} = $c;
38 $scripts{circ_duration} = $d;
39 $scripts{circ_recurring_fines}= $f;
40 $scripts{circ_max_fines} = $m;
41 $scripts{circ_permit_renew} = $pr;
43 $lb = [ $lb ] unless ref($lb);
47 "circulator: Loaded rules scripts for circ: " .
48 "circ permit patron = $p, ".
49 "circ permit copy = $c, ".
50 "circ duration = $d, ".
51 "circ recurring fines = $f, " .
52 "circ max fines = $m, ".
53 "circ renew permit = $pr. ".
55 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
60 __PACKAGE__->register_method(
61 method => "run_method",
62 api_name => "open-ils.circ.checkout.permit",
64 Determines if the given checkout can occur
65 @param authtoken The login session key
66 @param params A trailing hash of named params including
67 barcode : The copy barcode,
68 patron : The patron the checkout is occurring for,
69 renew : true or false - whether or not this is a renewal
70 @return The event that occurred during the permit check.
74 __PACKAGE__->register_method (
75 method => 'run_method',
76 api_name => 'open-ils.circ.checkout.permit.override',
77 signature => q/@see open-ils.circ.checkout.permit/,
81 __PACKAGE__->register_method(
82 method => "run_method",
83 api_name => "open-ils.circ.checkout",
86 @param authtoken The login session key
87 @param params A named hash of params including:
89 barcode If no copy is provided, the copy is retrieved via barcode
90 copyid If no copy or barcode is provide, the copy id will be use
91 patron The patron's id
92 noncat True if this is a circulation for a non-cataloted item
93 noncat_type The non-cataloged type id
94 noncat_circ_lib The location for the noncat circ.
95 precat The item has yet to be cataloged
96 dummy_title The temporary title of the pre-cataloded item
97 dummy_author The temporary authr of the pre-cataloded item
98 Default is the home org of the staff member
99 @return The SUCCESS event on success, any other event depending on the error
102 __PACKAGE__->register_method(
103 method => "run_method",
104 api_name => "open-ils.circ.checkin",
107 Generic super-method for handling all copies
108 @param authtoken The login session key
109 @param params Hash of named parameters including:
110 barcode - The copy barcode
111 force - If true, copies in bad statuses will be checked in and give good statuses
116 __PACKAGE__->register_method(
117 method => "run_method",
118 api_name => "open-ils.circ.checkin.override",
119 signature => q/@see open-ils.circ.checkin/
122 __PACKAGE__->register_method(
123 method => "run_method",
124 api_name => "open-ils.circ.renew.override",
125 signature => q/@see open-ils.circ.renew/,
129 __PACKAGE__->register_method(
130 method => "run_method",
131 api_name => "open-ils.circ.renew",
132 notes => <<" NOTES");
133 PARAMS( authtoken, circ => circ_id );
134 open-ils.circ.renew(login_session, circ_object);
135 Renews the provided circulation. login_session is the requestor of the
136 renewal and if the logged in user is not the same as circ->usr, then
137 the logged in user must have RENEW_CIRC permissions.
140 __PACKAGE__->register_method(
141 method => "run_method",
142 api_name => "open-ils.circ.checkout.full");
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkout.full.override");
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.inspect",
151 Returns the circ matrix test result and, on success, the rule set and matrix test object
158 my( $self, $conn, $auth, $args ) = @_;
159 translate_legacy_args($args);
160 my $api = $self->api_name;
163 OpenILS::Application::Circ::Circulator->new($auth, %$args);
165 return circ_events($circulator) if $circulator->bail_out;
167 # --------------------------------------------------------------------------
168 # Go ahead and load the script runner to make sure we have all
169 # of the objects we need
170 # --------------------------------------------------------------------------
171 $circulator->is_renewal(1) if $api =~ /renew/;
172 $circulator->is_checkin(1) if $api =~ /checkin/;
173 $circulator->check_penalty_on_renew(1) if
174 $circulator->is_renewal and $U->ou_ancestor_setting_value(
175 $circulator->editor->requestor->ws_ou, 'circ.renew.check_penalty', $circulator->editor);
177 if($legacy_script_support and not $circulator->is_checkin) {
178 $circulator->mk_script_runner();
179 $circulator->legacy_script_support(1);
180 $circulator->circ_permit_patron($scripts{circ_permit_patron});
181 $circulator->circ_permit_copy($scripts{circ_permit_copy});
182 $circulator->circ_duration($scripts{circ_duration});
183 $circulator->circ_permit_renew($scripts{circ_permit_renew});
185 $circulator->mk_env();
187 return circ_events($circulator) if $circulator->bail_out;
190 $circulator->override(1) if $api =~ /override/o;
192 if( $api =~ /checkout\.permit/ ) {
193 $circulator->do_permit();
195 } elsif( $api =~ /checkout.full/ ) {
197 $circulator->do_permit();
198 unless( $circulator->bail_out ) {
199 $circulator->events([]);
200 $circulator->do_checkout();
203 } elsif( $api =~ /inspect/ ) {
204 my $data = $circulator->do_inspect();
205 $circulator->editor->rollback;
208 } elsif( $api =~ /checkout/ ) {
209 $circulator->do_checkout();
211 } elsif( $api =~ /checkin/ ) {
212 $circulator->do_checkin();
214 } elsif( $api =~ /renew/ ) {
215 $circulator->is_renewal(1);
216 $circulator->do_renew();
219 if( $circulator->bail_out ) {
222 # make sure no success event accidentally slip in
224 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
227 my @e = @{$circulator->events};
228 push( @ee, $_->{textcode} ) for @e;
229 $logger->info("circulator: bailing out with events: @ee");
231 $circulator->editor->rollback;
234 $circulator->editor->commit;
237 $circulator->script_runner->cleanup if $circulator->script_runner;
239 $conn->respond_complete(circ_events($circulator));
241 unless($circulator->bail_out) {
242 $circulator->do_hold_notify($circulator->notify_hold)
243 if $circulator->notify_hold;
244 $circulator->retarget_holds if $circulator->retarget;
250 my @e = @{$circ->events};
251 # if we have multiple events, SUCCESS should not be one of them;
252 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
253 return (@e == 1) ? $e[0] : \@e;
257 sub translate_legacy_args {
260 if( $$args{barcode} ) {
261 $$args{copy_barcode} = $$args{barcode};
262 delete $$args{barcode};
265 if( $$args{copyid} ) {
266 $$args{copy_id} = $$args{copyid};
267 delete $$args{copyid};
270 if( $$args{patronid} ) {
271 $$args{patron_id} = $$args{patronid};
272 delete $$args{patronid};
275 if( $$args{patron} and !ref($$args{patron}) ) {
276 $$args{patron_id} = $$args{patron};
277 delete $$args{patron};
281 if( $$args{noncat} ) {
282 $$args{is_noncat} = $$args{noncat};
283 delete $$args{noncat};
286 if( $$args{precat} ) {
287 $$args{is_precat} = $$args{precat};
288 delete $$args{precat};
294 # --------------------------------------------------------------------------
295 # This package actually manages all of the circulation logic
296 # --------------------------------------------------------------------------
297 package OpenILS::Application::Circ::Circulator;
298 use strict; use warnings;
299 use vars q/$AUTOLOAD/;
301 use OpenILS::Utils::Fieldmapper;
302 use OpenSRF::Utils::Cache;
303 use Digest::MD5 qw(md5_hex);
304 use DateTime::Format::ISO8601;
305 use OpenILS::Utils::PermitHold;
306 use OpenSRF::Utils qw/:datetime/;
307 use OpenSRF::Utils::SettingsClient;
308 use OpenILS::Application::Circ::Holds;
309 use OpenILS::Application::Circ::Transit;
310 use OpenSRF::Utils::Logger qw(:logger);
311 use OpenILS::Utils::CStoreEditor qw/:funcs/;
312 use OpenILS::Application::Circ::ScriptBuilder;
313 use OpenILS::Const qw/:const/;
315 my $holdcode = "OpenILS::Application::Circ::Holds";
316 my $transcode = "OpenILS::Application::Circ::Transit";
321 # --------------------------------------------------------------------------
322 # Add a pile of automagic getter/setter methods
323 # --------------------------------------------------------------------------
324 my @AUTOLOAD_FIELDS = qw/
339 check_penalty_on_renew
366 recurring_fines_level
379 cancelled_hold_transit
388 legacy_script_support
394 my $type = ref($self) or die "$self is not an object";
396 my $name = $AUTOLOAD;
399 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
400 $logger->error("circulator: $type: invalid autoload field: $name");
401 die "$type: invalid autoload field: $name\n"
406 *{"${type}::${name}"} = sub {
409 $s->{$name} = $v if defined $v;
413 return $self->$name($data);
418 my( $class, $auth, %args ) = @_;
419 $class = ref($class) || $class;
420 my $self = bless( {}, $class );
424 new_editor(xact => 1, authtoken => $auth) );
426 unless( $self->editor->checkauth ) {
427 $self->bail_on_events($self->editor->event);
431 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
433 $self->$_($args{$_}) for keys %args;
436 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
438 # if this is a renewal, default to desk_renewal
439 $self->desk_renewal(1) unless
440 $self->opac_renewal or $self->phone_renewal;
446 # --------------------------------------------------------------------------
447 # True if we should discontinue processing
448 # --------------------------------------------------------------------------
450 my( $self, $bool ) = @_;
451 if( defined $bool ) {
452 $logger->info("circulator: BAILING OUT") if $bool;
453 $self->{bail_out} = $bool;
455 return $self->{bail_out};
460 my( $self, @evts ) = @_;
463 $logger->info("circulator: pushing event ".$e->{textcode});
464 push( @{$self->events}, $e ) unless
465 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
471 my $key = md5_hex( time() . rand() . "$$" );
472 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
473 return $self->permit_key($key);
476 sub check_permit_key {
478 my $key = $self->permit_key;
479 return 0 unless $key;
480 my $k = "oils_permit_key_$key";
481 my $one = $self->cache_handle->get_cache($k);
482 $self->cache_handle->delete_cache($k);
483 return ($one) ? 1 : 0;
488 my $e = $self->editor;
490 # --------------------------------------------------------------------------
491 # Grab the fleshed copy
492 # --------------------------------------------------------------------------
493 unless($self->is_noncat) {
497 flesh_fields => {acp => ['call_number'], acn => ['record']}
500 $copy = $e->retrieve_asset_copy(
501 [$self->copy_id, $flesh ]) or return $e->event;
503 } elsif( $self->copy_barcode ) {
505 $copy = $e->search_asset_copy(
506 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0]
512 $self->volume($copy->call_number);
513 $self->title($self->volume->record);
514 $self->copy->call_number($self->volume->id);
515 $self->volume->record($self->title->id);
516 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
518 # We can't renew if there is no copy
519 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
520 if $self->is_renewal;
526 # --------------------------------------------------------------------------
528 # --------------------------------------------------------------------------
531 if( $self->patron_id ) {
532 $patron = $e->retrieve_actor_user($self->patron_id) or $p_evt = $e->event;
534 } elsif( $self->patron_barcode ) {
536 my $card = $e->search_actor_card(
537 {barcode => $self->patron_barcode})->[0] or return $e->event;
539 $patron = $e->search_actor_user(
540 {card => $card->id})->[0] or return $e->event;
543 if( my $copy = $self->copy ) {
544 my $circs = $e->search_action_circulation(
545 {target_copy => $copy->id, checkin_time => undef});
547 if( my $circ = $circs->[0] ) {
548 $patron = $e->retrieve_actor_user($circ->usr)
554 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
555 unless $self->patron($patron);
558 # --------------------------------------------------------------------------
559 # This builds the script runner environment and fetches most of the
561 # --------------------------------------------------------------------------
562 sub mk_script_runner {
568 qw/copy copy_barcode copy_id patron
569 patron_id patron_barcode volume title editor/;
571 # Translate our objects into the ScriptBuilder args hash
572 $$args{$_} = $self->$_() for @fields;
574 $args->{ignore_user_status} = 1 if $self->is_checkin;
575 $$args{fetch_patron_by_circ_copy} = 1;
576 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
578 if( my $pco = $self->pending_checkouts ) {
579 $logger->info("circulator: we were given a pending checkouts number of $pco");
580 $$args{patronItemsOut} = $pco;
583 # This fetches most of the objects we need
584 $self->script_runner(
585 OpenILS::Application::Circ::ScriptBuilder->build($args));
587 # Now we translate the ScriptBuilder objects back into self
588 $self->$_($$args{$_}) for @fields;
590 my @evts = @{$args->{_events}} if $args->{_events};
592 $logger->debug("circulator: script builder returned events: @evts") if @evts;
596 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
597 if(!$self->is_noncat and
599 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
603 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
604 return $self->bail_on_events(@e);
608 $self->is_precat(1) if $self->copy
609 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
611 # We can't renew if there is no copy
612 return $self->bail_on_events(@evts) if
613 $self->is_renewal and !$self->copy;
615 # Set some circ-specific flags in the script environment
616 my $evt = "environment";
617 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
619 if( $self->is_noncat ) {
620 $self->script_runner->insert("$evt.isNonCat", 1);
621 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
624 if( $self->is_precat ) {
625 $self->script_runner->insert("environment.isPrecat", 1, 1);
628 $self->script_runner->add_path( $_ ) for @$script_libs;
633 # --------------------------------------------------------------------------
634 # Does the circ permit work
635 # --------------------------------------------------------------------------
639 $self->log_me("do_permit()");
641 unless( $self->editor->requestor->id == $self->patron->id ) {
642 return $self->bail_on_events($self->editor->event)
643 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
646 $self->check_captured_holds();
647 $self->do_copy_checks();
648 return if $self->bail_out;
649 $self->run_patron_permit_scripts();
650 $self->run_copy_permit_scripts()
651 unless $self->is_precat or $self->is_noncat;
652 $self->override_events() unless
653 $self->is_renewal and not $self->check_penalty_on_renew;
654 return if $self->bail_out;
656 if( $self->is_precat ) {
659 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
660 return $self->bail_out(1) unless $self->is_renewal;
666 payload => $self->mk_permit_key));
670 sub check_captured_holds {
672 my $copy = $self->copy;
673 my $patron = $self->patron;
675 return undef unless $copy;
677 my $s = $U->copy_status($copy->status)->id;
678 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
679 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
681 # Item is on the holds shelf, make sure it's going to the right person
682 my $holds = $self->editor->search_action_hold_request(
685 current_copy => $copy->id ,
686 capture_time => { '!=' => undef },
687 cancel_time => undef,
688 fulfillment_time => undef
694 if( $holds and $$holds[0] ) {
695 return undef if $$holds[0]->usr == $patron->id;
698 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
700 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
706 my $copy = $self->copy;
709 my $stat = $U->copy_status($copy->status)->id;
711 # We cannot check out a copy if it is in-transit
712 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
713 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
716 $self->handle_claims_returned();
717 return if $self->bail_out;
719 # no claims returned circ was found, check if there is any open circ
720 unless( $self->is_renewal ) {
721 my $circs = $self->editor->search_action_circulation(
722 { target_copy => $copy->id, checkin_time => undef }
725 return $self->bail_on_events(
726 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
731 sub send_penalty_request {
733 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
734 $self->penalty_request(
736 'open-ils.penalty.patron_penalty.calculate',
738 authtoken => $self->editor->authtoken,
739 patron => $self->patron } ) );
742 sub gather_penalty_request {
744 return [] unless $self->penalty_request;
745 my $data = $self->penalty_request->recv;
747 throw $data if UNIVERSAL::isa($data,'Error');
748 $data = $data->content;
749 return $data->{fatal_penalties};
751 $logger->error("circulator: penalty request returned no data");
755 my $LEGACY_CIRC_EVENT_MAP = {
756 'actor.usr.barred' => 'PATRON_BARRED',
757 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
758 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
759 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
760 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
761 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
762 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
763 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
764 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
768 # ---------------------------------------------------------------------
769 # This pushes any patron-related events into the list but does not
770 # set bail_out for any events
771 # ---------------------------------------------------------------------
772 sub run_patron_permit_scripts {
774 my $runner = $self->script_runner;
775 my $patronid = $self->patron->id;
779 if(!$self->legacy_script_support) {
781 my $results = $self->run_indb_circ_test;
782 unless($self->circ_test_success) {
783 push(@allevents, OpenILS::Event->new(
784 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
789 $self->send_penalty_request() unless
790 $self->is_renewal and not $self->check_penalty_on_renew;
793 # --------------------------------------------------------------------- # Now run the patron permit script
794 # ---------------------------------------------------------------------
795 $runner->load($self->circ_permit_patron);
796 my $result = $runner->run or
797 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
799 my $patron_events = $result->{events};
801 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
802 [] : $self->gather_penalty_request();
804 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
807 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
809 $self->push_events(@allevents);
812 sub run_indb_circ_test {
814 return $self->matrix_test_result if $self->matrix_test_result;
816 my $dbfunc = ($self->is_renewal) ?
817 'action.item_renew_circ_test' : 'action.item_user_circ_test';
819 my $results = ($self->matrix_test_result) ?
820 $self->matrix_test_result :
821 $self->editor->json_query(
824 $self->editor->requestor->ws_ou,
831 $self->circ_test_success($U->is_true($results->[0]->{success}));
832 if($self->circ_test_success) {
833 $self->circ_matrix_test(
834 $self->editor->retrieve_config_circ_matrix_test(
835 $results->[0]->{matchpoint}
840 if($self->circ_test_success) {
841 $self->circ_matrix_ruleset(
842 $self->editor->retrieve_config_circ_matrix_ruleset([
843 $results->[0]->{matchpoint},
846 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
854 return $self->matrix_test_result($results);
859 $self->run_indb_circ_test;
861 circ_test_success => $self->circ_test_success,
862 matrix_test_result => $self->matrix_test_result,
863 circ_matrix_test => $self->circ_matrix_test,
864 circ_matrix_ruleset => $self->circ_matrix_ruleset
869 sub run_copy_permit_scripts {
871 my $copy = $self->copy || return;
872 my $runner = $self->script_runner;
876 if(!$self->legacy_script_support) {
877 my $results = $self->run_indb_circ_test;
878 unless($self->circ_test_success) {
879 push(@allevents, OpenILS::Event->new(
880 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
884 # ---------------------------------------------------------------------
885 # Capture all of the copy permit events
886 # ---------------------------------------------------------------------
887 $runner->load($self->circ_permit_copy);
888 my $result = $runner->run or
889 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
890 my $copy_events = $result->{events};
892 # ---------------------------------------------------------------------
893 # Now collect all of the events together
894 # ---------------------------------------------------------------------
895 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
898 # See if this copy has an alert message
899 my $ae = $self->check_copy_alert();
900 push( @allevents, $ae ) if $ae;
902 # uniquify the events
903 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
904 @allevents = values %hash;
907 $_->{payload} = $copy if
908 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
911 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
913 $self->push_events(@allevents);
917 sub check_copy_alert {
919 return undef if $self->is_renewal;
920 return OpenILS::Event->new(
921 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
922 if $self->copy and $self->copy->alert_message;
928 # --------------------------------------------------------------------------
929 # If the call is overriding and has permissions to override every collected
930 # event, the are cleared. Any event that the caller does not have
931 # permission to override, will be left in the event list and bail_out will
933 # XXX We need code in here to cancel any holds/transits on copies
934 # that are being force-checked out
935 # --------------------------------------------------------------------------
936 sub override_events {
938 my @events = @{$self->events};
939 return unless @events;
941 if(!$self->override) {
942 return $self->bail_out(1)
943 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
948 for my $e (@events) {
949 my $tc = $e->{textcode};
950 next if $tc eq 'SUCCESS';
951 my $ov = "$tc.override";
952 $logger->info("circulator: attempting to override event: $ov");
954 return $self->bail_on_events($self->editor->event)
955 unless( $self->editor->allowed($ov) );
960 # --------------------------------------------------------------------------
961 # If there is an open claimsreturn circ on the requested copy, close the
962 # circ if overriding, otherwise bail out
963 # --------------------------------------------------------------------------
964 sub handle_claims_returned {
966 my $copy = $self->copy;
968 my $CR = $self->editor->search_action_circulation(
970 target_copy => $copy->id,
971 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
972 checkin_time => undef,
976 return unless ($CR = $CR->[0]);
980 # - If the caller has set the override flag, we will check the item in
981 if($self->override) {
983 $CR->checkin_time('now');
984 $CR->checkin_lib($self->editor->requestor->ws_ou);
985 $CR->checkin_staff($self->editor->requestor->id);
987 $evt = $self->editor->event
988 unless $self->editor->update_action_circulation($CR);
991 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
994 $self->bail_on_events($evt) if $evt;
999 # --------------------------------------------------------------------------
1000 # This performs the checkout
1001 # --------------------------------------------------------------------------
1005 $self->log_me("do_checkout()");
1007 # make sure perms are good if this isn't a renewal
1008 unless( $self->is_renewal ) {
1009 return $self->bail_on_events($self->editor->event)
1010 unless( $self->editor->allowed('COPY_CHECKOUT') );
1013 # verify the permit key
1014 unless( $self->check_permit_key ) {
1015 if( $self->permit_override ) {
1016 return $self->bail_on_events($self->editor->event)
1017 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1019 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1023 # if this is a non-cataloged circ, build the circ and finish
1024 if( $self->is_noncat ) {
1025 $self->checkout_noncat;
1027 OpenILS::Event->new('SUCCESS',
1028 payload => { noncat_circ => $self->circ }));
1032 if( $self->is_precat ) {
1033 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1034 $self->make_precat_copy;
1035 return if $self->bail_out;
1037 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1038 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1041 $self->do_copy_checks;
1042 return if $self->bail_out;
1044 $self->run_checkout_scripts();
1045 return if $self->bail_out;
1047 $self->build_checkout_circ_object();
1048 return if $self->bail_out;
1050 $self->apply_modified_due_date();
1051 return if $self->bail_out;
1053 return $self->bail_on_events($self->editor->event)
1054 unless $self->editor->create_action_circulation($self->circ);
1056 # refresh the circ to force local time zone for now
1057 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1059 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1061 return if $self->bail_out;
1063 $self->handle_checkout_holds();
1064 return if $self->bail_out;
1066 # ------------------------------------------------------------------------------
1067 # Update the patron penalty info in the DB. Run it for permit-overrides or
1068 # renewals since both of those cases do not require the penalty server to
1069 # run during the permit phase of the checkout
1070 # ------------------------------------------------------------------------------
1071 if( $self->permit_override or $self->is_renewal ) {
1072 $U->update_patron_penalties(
1073 authtoken => $self->editor->authtoken,
1074 patron => $self->patron,
1079 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1081 OpenILS::Event->new('SUCCESS',
1083 copy => $U->unflesh_copy($self->copy),
1084 circ => $self->circ,
1086 holds_fulfilled => $self->fulfilled_holds,
1094 my $copy = $self->copy;
1096 my $stat = $copy->status if ref $copy->status;
1097 my $loc = $copy->location if ref $copy->location;
1098 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1100 $copy->status($stat->id) if $stat;
1101 $copy->location($loc->id) if $loc;
1102 $copy->circ_lib($circ_lib->id) if $circ_lib;
1103 $copy->editor($self->editor->requestor->id);
1104 $copy->edit_date('now');
1105 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1107 return $self->bail_on_events($self->editor->event)
1108 unless $self->editor->update_asset_copy($self->copy);
1110 $copy->status($U->copy_status($copy->status));
1111 $copy->location($loc) if $loc;
1112 $copy->circ_lib($circ_lib) if $circ_lib;
1116 sub bail_on_events {
1117 my( $self, @evts ) = @_;
1118 $self->push_events(@evts);
1122 sub handle_checkout_holds {
1125 my $copy = $self->copy;
1126 my $patron = $self->patron;
1128 my $holds = $self->editor->search_action_hold_request(
1130 current_copy => $copy->id ,
1131 cancel_time => undef,
1132 fulfillment_time => undef
1138 # XXX We should only fulfill one hold here...
1139 # XXX If a hold was transited to the user who is checking out
1140 # the item, we need to make sure that hold is what's grabbed
1143 # for now, just sort by id to get what should be the oldest hold
1144 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1145 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1146 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1149 my $hold = $myholds[0];
1151 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1153 # if the hold was never officially captured, capture it.
1154 $hold->capture_time('now') unless $hold->capture_time;
1156 # just make sure it's set correctly
1157 $hold->current_copy($copy->id);
1159 $hold->fulfillment_time('now');
1160 $hold->fulfillment_staff($self->editor->requestor->id);
1161 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1163 return $self->bail_on_events($self->editor->event)
1164 unless $self->editor->update_action_hold_request($hold);
1166 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1168 push( @fulfilled, $hold->id );
1171 # If there are any holds placed for other users that point to this copy,
1172 # then we need to un-target those holds so the targeter can pick a new copy
1175 $logger->info("circulator: un-targeting hold ".$_->id.
1176 " because copy ".$copy->id." is getting checked out");
1178 # - make the targeter process this hold at next run
1179 $_->clear_prev_check_time;
1181 # - clear out the targetted copy
1182 $_->clear_current_copy;
1183 $_->clear_capture_time;
1185 return $self->bail_on_event($self->editor->event)
1186 unless $self->editor->update_action_hold_request($_);
1190 $self->fulfilled_holds(\@fulfilled);
1195 sub run_checkout_scripts {
1199 my $runner = $self->script_runner;
1208 if(!$self->legacy_script_support) {
1209 $self->run_indb_circ_test();
1210 $duration = $self->circ_matrix_ruleset->duration_rule;
1211 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1212 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1216 $runner->load($self->circ_duration);
1218 my $result = $runner->run or
1219 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1221 $duration_name = $result->{durationRule};
1222 $recurring_name = $result->{recurringFinesRule};
1223 $max_fine_name = $result->{maxFine};
1226 $duration_name = $duration->name if $duration;
1227 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1230 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1231 return $self->bail_on_events($evt) if $evt;
1233 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1234 return $self->bail_on_events($evt) if $evt;
1236 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1237 return $self->bail_on_events($evt) if $evt;
1242 # The item circulates with an unlimited duration
1248 $self->duration_rule($duration);
1249 $self->recurring_fines_rule($recurring);
1250 $self->max_fine_rule($max_fine);
1254 sub build_checkout_circ_object {
1257 my $circ = Fieldmapper::action::circulation->new;
1258 my $duration = $self->duration_rule;
1259 my $max = $self->max_fine_rule;
1260 my $recurring = $self->recurring_fines_rule;
1261 my $copy = $self->copy;
1262 my $patron = $self->patron;
1266 my $dname = $duration->name;
1267 my $mname = $max->name;
1268 my $rname = $recurring->name;
1270 my $max_amount = $max->amount;
1272 # if is_percent is true then the max->amount is
1273 # use as a percentage of the copy price
1274 if ($U->is_true($max->is_percent)) {
1276 my $cn = $self->editor->retrieve_asset_call_number($copy->call_number);
1278 my $default_price = $U->ou_ancestor_setting_value(
1279 $cn->owning_lib, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
1280 my $charge_on_0 = $U->ou_ancestor_setting_value(
1281 $cn->owning_lib, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
1283 # Find the most appropriate "price" -- same definition as the
1284 # LOST price. See OpenILS::Circ::new_set_circ_lost
1285 $max_amount = $copy->price;
1286 $max_amount = $default_price unless defined $max_amount;
1287 $max_amount = 0 if $max_amount < 0;
1288 $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
1290 $max_amount *= $max->amount / 100;
1294 $logger->debug("circulator: building circulation ".
1295 "with duration=$dname, maxfine=$mname, recurring=$rname");
1297 $circ->duration( $duration->shrt )
1298 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1299 $circ->duration( $duration->normal )
1300 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1301 $circ->duration( $duration->extended )
1302 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1304 $circ->recuring_fine( $recurring->low )
1305 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1306 $circ->recuring_fine( $recurring->normal )
1307 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1308 $circ->recuring_fine( $recurring->high )
1309 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1311 $circ->duration_rule( $duration->name );
1312 $circ->recuring_fine_rule( $recurring->name );
1313 $circ->max_fine_rule( $max->name );
1315 $circ->max_fine( $max_amount );
1317 $circ->fine_interval($recurring->recurance_interval);
1318 $circ->renewal_remaining( $duration->max_renewals );
1322 $logger->info("circulator: copy found with an unlimited circ duration");
1323 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1324 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1325 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1326 $circ->renewal_remaining(0);
1329 $circ->target_copy( $copy->id );
1330 $circ->usr( $patron->id );
1331 $circ->circ_lib( $self->circ_lib );
1333 if( $self->is_renewal ) {
1334 $circ->opac_renewal('t') if $self->opac_renewal;
1335 $circ->phone_renewal('t') if $self->phone_renewal;
1336 $circ->desk_renewal('t') if $self->desk_renewal;
1337 $circ->renewal_remaining($self->renewal_remaining);
1338 $circ->circ_staff($self->editor->requestor->id);
1342 # if the user provided an overiding checkout time,
1343 # (e.g. the checkout really happened several hours ago), then
1344 # we apply that here. Does this need a perm??
1345 $circ->xact_start(clense_ISO8601($self->checkout_time))
1346 if $self->checkout_time;
1348 # if a patron is renewing, 'requestor' will be the patron
1349 $circ->circ_staff($self->editor->requestor->id);
1350 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1356 sub apply_modified_due_date {
1358 my $circ = $self->circ;
1359 my $copy = $self->copy;
1361 if( $self->due_date ) {
1363 return $self->bail_on_events($self->editor->event)
1364 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1366 $circ->due_date(clense_ISO8601($self->due_date));
1370 # if the due_date lands on a day when the location is closed
1371 return unless $copy and $circ->due_date;
1373 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1375 # due-date overlap should be determined by the location the item
1376 # is checked out from, not the owning or circ lib of the item
1377 my $org = $self->editor->requestor->ws_ou;
1379 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1380 " with an item due date of ".$circ->due_date );
1382 my $dateinfo = $U->storagereq(
1383 'open-ils.storage.actor.org_unit.closed_date.overlap',
1384 $org, $circ->due_date );
1387 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1388 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1390 # XXX make the behavior more dynamic
1391 # for now, we just push the due date to after the close date
1392 $circ->due_date($dateinfo->{end});
1399 sub create_due_date {
1400 my( $self, $duration ) = @_;
1401 # if there is a raw time component (e.g. from postgres),
1402 # turn it into an interval that interval_to_seconds can parse
1403 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1404 my ($sec,$min,$hour,$mday,$mon,$year) =
1405 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1406 $year += 1900; $mon += 1;
1407 my $due_date = sprintf(
1408 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1409 $year, $mon, $mday, $hour, $min, $sec);
1415 sub make_precat_copy {
1417 my $copy = $self->copy;
1420 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1422 $copy->editor($self->editor->requestor->id);
1423 $copy->edit_date('now');
1424 $copy->dummy_title($self->dummy_title);
1425 $copy->dummy_author($self->dummy_author);
1427 $self->update_copy();
1431 $logger->info("circulator: Creating a new precataloged ".
1432 "copy in checkout with barcode " . $self->copy_barcode);
1434 $copy = Fieldmapper::asset::copy->new;
1435 $copy->circ_lib($self->circ_lib);
1436 $copy->creator($self->editor->requestor->id);
1437 $copy->editor($self->editor->requestor->id);
1438 $copy->barcode($self->copy_barcode);
1439 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1440 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1441 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1443 $copy->dummy_title($self->dummy_title || "");
1444 $copy->dummy_author($self->dummy_author || "");
1446 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1448 $self->push_events($self->editor->event);
1452 # this is a little bit of a hack, but we need to
1453 # get the copy into the script runner
1454 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1458 sub checkout_noncat {
1464 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1465 my $count = $self->noncat_count || 1;
1466 my $cotime = clense_ISO8601($self->checkout_time) || "";
1468 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1472 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1473 $self->editor->requestor->id,
1481 $self->push_events($evt);
1492 $self->log_me("do_checkin()");
1495 return $self->bail_on_events(
1496 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1499 if( $self->checkin_check_holds_shelf() ) {
1500 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1501 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1502 $self->checkin_flesh_events;
1506 unless( $self->is_renewal ) {
1507 return $self->bail_on_events($self->editor->event)
1508 unless $self->editor->allowed('COPY_CHECKIN');
1511 $self->push_events($self->check_copy_alert());
1512 $self->push_events($self->check_checkin_copy_status());
1514 # the renew code will have already found our circulation object
1515 unless( $self->is_renewal and $self->circ ) {
1516 my $circs = $self->editor->search_action_circulation(
1517 { target_copy => $self->copy->id, checkin_time => undef });
1518 $self->circ($$circs[0]);
1520 # for now, just warn if there are multiple open circs on a copy
1521 $logger->warn("circulator: we have ".scalar(@$circs).
1522 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1525 # if the circ is marked as 'claims returned', add the event to the list
1526 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1527 if ($self->circ and $self->circ->stop_fines
1528 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1530 # handle the overridable events
1531 $self->override_events unless $self->is_renewal;
1532 return if $self->bail_out;
1536 $self->editor->search_action_transit_copy(
1537 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1541 $self->checkin_handle_circ;
1542 return if $self->bail_out;
1543 $self->checkin_changed(1);
1545 } elsif( $self->transit ) {
1546 my $hold_transit = $self->process_received_transit;
1547 $self->checkin_changed(1);
1549 if( $self->bail_out ) {
1550 $self->checkin_flesh_events;
1554 if( my $e = $self->check_checkin_copy_status() ) {
1555 # If the original copy status is special, alert the caller
1556 my $ev = $self->events;
1557 $self->events([$e]);
1558 $self->override_events;
1559 return if $self->bail_out;
1563 if( $hold_transit or
1564 $U->copy_status($self->copy->status)->id
1565 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1568 if( $hold_transit ) {
1569 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1571 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1576 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1578 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1579 $self->reshelve_copy(1);
1580 $self->cancelled_hold_transit(1);
1581 $self->notify_hold(0); # don't notify for cancelled holds
1582 return if $self->bail_out;
1586 # hold transited to correct location
1587 $self->checkin_flesh_events;
1592 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1594 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1595 " that is in-transit, but there is no transit.. repairing");
1596 $self->reshelve_copy(1);
1597 return if $self->bail_out;
1600 if( $self->is_renewal ) {
1601 $self->push_events(OpenILS::Event->new('SUCCESS'));
1605 # ------------------------------------------------------------------------------
1606 # Circulations and transits are now closed where necessary. Now go on to see if
1607 # this copy can fulfill a hold or needs to be routed to a different location
1608 # ------------------------------------------------------------------------------
1610 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1611 return if $self->bail_out;
1613 } else { # not needed for a hold
1615 my $circ_lib = (ref $self->copy->circ_lib) ?
1616 $self->copy->circ_lib->id : $self->copy->circ_lib;
1618 if( $self->remote_hold ) {
1619 $circ_lib = $self->remote_hold->pickup_lib;
1620 $logger->warn("circulator: Copy ".$self->copy->barcode.
1621 " is on a remote hold's shelf, sending to $circ_lib");
1624 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1626 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1628 $self->checkin_handle_precat();
1629 return if $self->bail_out;
1633 my $bc = $self->copy->barcode;
1634 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1635 $self->checkin_build_copy_transit($circ_lib);
1636 return if $self->bail_out;
1637 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1641 $self->reshelve_copy;
1642 return if $self->bail_out;
1644 unless($self->checkin_changed) {
1646 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1647 my $stat = $U->copy_status($self->copy->status)->id;
1649 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1650 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1651 $self->bail_out(1); # no need to commit anything
1655 $self->push_events(OpenILS::Event->new('SUCCESS'))
1656 unless @{$self->events};
1660 # ------------------------------------------------------------------------------
1661 # Update the patron penalty info in the DB
1662 # ------------------------------------------------------------------------------
1663 $U->update_patron_penalties(
1664 authtoken => $self->editor->authtoken,
1665 patron => $self->patron,
1666 background => 1 ) if $self->is_checkin;
1668 $self->checkin_flesh_events;
1674 my $force = $self->force || shift;
1675 my $copy = $self->copy;
1677 my $stat = $U->copy_status($copy->status)->id;
1680 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1681 $stat != OILS_COPY_STATUS_CATALOGING and
1682 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1683 $stat != OILS_COPY_STATUS_RESHELVING )) {
1685 $copy->status( OILS_COPY_STATUS_RESHELVING );
1687 $self->checkin_changed(1);
1692 # Returns true if the item is at the current location
1693 # because it was transited there for a hold and the
1694 # hold has not been fulfilled
1695 sub checkin_check_holds_shelf {
1697 return 0 unless $self->copy;
1700 $U->copy_status($self->copy->status)->id ==
1701 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1703 # find the hold that put us on the holds shelf
1704 my $holds = $self->editor->search_action_hold_request(
1706 current_copy => $self->copy->id,
1707 capture_time => { '!=' => undef },
1708 fulfillment_time => undef,
1709 cancel_time => undef,
1714 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1715 $self->reshelve_copy(1);
1719 my $hold = $$holds[0];
1721 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1722 $hold->id. "] for copy ".$self->copy->barcode);
1724 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1725 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1729 $logger->info("circulator: hold is not for here..");
1730 $self->remote_hold($hold);
1735 sub checkin_handle_precat {
1737 my $copy = $self->copy;
1739 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1740 $copy->status(OILS_COPY_STATUS_CATALOGING);
1741 $self->update_copy();
1742 $self->checkin_changed(1);
1743 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1748 sub checkin_build_copy_transit {
1751 my $copy = $self->copy;
1752 my $transit = Fieldmapper::action::transit_copy->new;
1754 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1755 $logger->info("circulator: transiting copy to $dest");
1757 $transit->source($self->editor->requestor->ws_ou);
1758 $transit->dest($dest);
1759 $transit->target_copy($copy->id);
1760 $transit->source_send_time('now');
1761 $transit->copy_status( $U->copy_status($copy->status)->id );
1763 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1765 return $self->bail_on_events($self->editor->event)
1766 unless $self->editor->create_action_transit_copy($transit);
1768 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1770 $self->checkin_changed(1);
1774 sub attempt_checkin_hold_capture {
1776 my $copy = $self->copy;
1778 # See if this copy can fulfill any holds
1779 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1780 $self->editor, $copy, $self->editor->requestor );
1783 $logger->debug("circulator: no potential permitted".
1784 "holds found for copy ".$copy->barcode);
1788 $self->retarget($retarget);
1790 $logger->info("circulator: found permitted hold ".
1791 $hold->id . " for copy, capturing...");
1793 $hold->current_copy($copy->id);
1794 $hold->capture_time('now');
1796 # prevent DB errors caused by fetching
1797 # holds from storage, and updating through cstore
1798 $hold->clear_fulfillment_time;
1799 $hold->clear_fulfillment_staff;
1800 $hold->clear_fulfillment_lib;
1801 $hold->clear_expire_time;
1802 $hold->clear_cancel_time;
1803 $hold->clear_prev_check_time unless $hold->prev_check_time;
1805 $self->bail_on_events($self->editor->event)
1806 unless $self->editor->update_action_hold_request($hold);
1808 $self->checkin_changed(1);
1810 return 1 if $self->bail_out;
1812 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1814 # This hold was captured in the correct location
1815 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1816 $self->push_events(OpenILS::Event->new('SUCCESS'));
1818 #$self->do_hold_notify($hold->id);
1819 $self->notify_hold($hold->id);
1823 # Hold needs to be picked up elsewhere. Build a hold
1824 # transit and route the item.
1825 $self->checkin_build_hold_transit();
1826 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1827 return 1 if $self->bail_out;
1829 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1832 # make sure we save the copy status
1837 sub do_hold_notify {
1838 my( $self, $holdid ) = @_;
1840 $logger->info("circulator: running delayed hold notify process");
1842 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1843 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1845 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1846 hold_id => $holdid, requestor => $self->editor->requestor);
1848 $logger->debug("circulator: built hold notifier");
1850 if(!$notifier->event) {
1852 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1854 my $stat = $notifier->send_email_notify;
1855 if( $stat == '1' ) {
1856 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1860 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1863 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1867 sub retarget_holds {
1868 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1869 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1870 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1871 # no reason to wait for the return value
1875 sub checkin_build_hold_transit {
1878 my $copy = $self->copy;
1879 my $hold = $self->hold;
1880 my $trans = Fieldmapper::action::hold_transit_copy->new;
1882 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1884 $trans->hold($hold->id);
1885 $trans->source($self->editor->requestor->ws_ou);
1886 $trans->dest($hold->pickup_lib);
1887 $trans->source_send_time("now");
1888 $trans->target_copy($copy->id);
1890 # when the copy gets to its destination, it will recover
1891 # this status - put it onto the holds shelf
1892 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1894 return $self->bail_on_events($self->editor->event)
1895 unless $self->editor->create_action_hold_transit_copy($trans);
1900 sub process_received_transit {
1902 my $copy = $self->copy;
1903 my $copyid = $self->copy->id;
1905 my $status_name = $U->copy_status($copy->status)->name;
1906 $logger->debug("circulator: attempting transit receive on ".
1907 "copy $copyid. Copy status is $status_name");
1909 my $transit = $self->transit;
1911 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1912 # - this item is in-transit to a different location
1914 my $tid = $transit->id;
1915 my $loc = $self->editor->requestor->ws_ou;
1916 my $dest = $transit->dest;
1918 $logger->info("circulator: Fowarding transit on copy which is destined ".
1919 "for a different location. transit=$tid, copy=$copyid, current ".
1920 "location=$loc, destination location=$dest");
1922 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1924 # grab the associated hold object if available
1925 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1926 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1928 return $self->bail_on_events($evt);
1931 # The transit is received, set the receive time
1932 $transit->dest_recv_time('now');
1933 $self->bail_on_events($self->editor->event)
1934 unless $self->editor->update_action_transit_copy($transit);
1936 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1938 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1939 $copy->status( $transit->copy_status );
1940 $self->update_copy();
1941 return if $self->bail_out;
1945 #$self->do_hold_notify($hold_transit->hold);
1946 $self->notify_hold($hold_transit->hold);
1951 OpenILS::Event->new(
1954 payload => { transit => $transit, holdtransit => $hold_transit } ));
1956 return $hold_transit;
1960 sub checkin_handle_circ {
1964 my $circ = $self->circ;
1965 my $copy = $self->copy;
1969 # backdate the circ if necessary
1970 if($self->backdate) {
1971 $self->checkin_handle_backdate;
1972 return if $self->bail_out;
1975 if(!$circ->stop_fines) {
1976 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1977 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1978 $circ->stop_fines_time('now') unless $self->backdate;
1979 $circ->stop_fines_time($self->backdate) if $self->backdate;
1982 # see if there are any fines owed on this circ. if not, close it
1983 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1984 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1986 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1988 # Set the checkin vars since we have the item
1989 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1991 $circ->checkin_staff($self->editor->requestor->id);
1992 $circ->checkin_lib($self->editor->requestor->ws_ou);
1994 my $circ_lib = (ref $self->copy->circ_lib) ?
1995 $self->copy->circ_lib->id : $self->copy->circ_lib;
1996 my $stat = $U->copy_status($self->copy->status)->id;
1998 # If the item is lost/missing and it needs to be sent home, don't
1999 # reshelve the copy, leave it lost/missing so the recipient will know
2000 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2001 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2002 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2005 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2009 return $self->bail_on_events($self->editor->event)
2010 unless $self->editor->update_action_circulation($circ);
2014 sub checkin_handle_backdate {
2017 my $bd = $self->backdate;
2019 # ------------------------------------------------------------------
2020 # clean up the backdate for date comparison
2021 # we want any bills created on or after the backdate
2022 # ------------------------------------------------------------------
2023 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2024 #$bd = "${bd}T23:59:59";
2026 my $bills = $self->editor->search_money_billing(
2028 billing_ts => { '>=' => $bd },
2029 xact => $self->circ->id,
2030 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2034 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2036 for my $bill (@$bills) {
2037 unless( $U->is_true($bill->voided) ) {
2038 $logger->info("backdate voiding bill ".$bill->id);
2040 $bill->void_time('now');
2041 $bill->voider($self->editor->requestor->id);
2042 my $n = $bill->note || "";
2043 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2045 $self->bail_on_events($self->editor->event)
2046 unless $self->editor->update_money_billing($bill);
2054 # XXX Legacy version for Circ.pm support
2055 sub _checkin_handle_backdate {
2056 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
2059 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2060 $bd = "${bd}T23:59:59";
2062 my $bills = $session->request(
2063 "open-ils.storage.direct.money.billing.search_where.atomic",
2064 billing_ts => { '>=' => $bd },
2066 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2069 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2072 for my $bill (@$bills) {
2073 unless( $U->is_true($bill->voided) ) {
2074 $logger->debug("voiding bill ".$bill->id);
2076 $bill->void_time('now');
2077 $bill->voider($requestor->id);
2078 my $n = $bill->note || "";
2079 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
2080 my $s = $session->request(
2081 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
2082 return $U->DB_UPDATE_FAILED($bill) unless $s;
2096 sub find_patron_from_copy {
2098 my $circs = $self->editor->search_action_circulation(
2099 { target_copy => $self->copy->id, checkin_time => undef });
2100 my $circ = $circs->[0];
2101 return unless $circ;
2102 my $u = $self->editor->retrieve_actor_user($circ->usr)
2103 or return $self->bail_on_events($self->editor->event);
2107 sub check_checkin_copy_status {
2109 my $copy = $self->copy;
2115 my $status = $U->copy_status($copy->status)->id;
2118 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2119 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2120 $status == OILS_COPY_STATUS_IN_PROCESS ||
2121 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2122 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2123 $status == OILS_COPY_STATUS_CATALOGING ||
2124 $status == OILS_COPY_STATUS_RESHELVING );
2126 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2127 if( $status == OILS_COPY_STATUS_LOST );
2129 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2130 if( $status == OILS_COPY_STATUS_MISSING );
2132 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2137 # --------------------------------------------------------------------------
2138 # On checkin, we need to return as many relevant objects as we can
2139 # --------------------------------------------------------------------------
2140 sub checkin_flesh_events {
2143 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2144 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2145 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2149 for my $evt (@{$self->events}) {
2152 $payload->{copy} = $U->unflesh_copy($self->copy);
2153 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2154 $payload->{circ} = $self->circ;
2155 $payload->{transit} = $self->transit;
2156 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2158 # $self->hold may or may not have been replaced with a
2159 # valid hold after processing a cancelled hold
2160 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2162 $evt->{payload} = $payload;
2167 my( $self, $msg ) = @_;
2168 my $bc = ($self->copy) ? $self->copy->barcode :
2171 my $usr = ($self->patron) ? $self->patron->id : "";
2172 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2173 ", recipient=$usr, copy=$bc");
2179 $self->log_me("do_renew()");
2180 $self->is_renewal(1);
2182 # Make sure there is an open circ to renew that is not
2183 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2184 my $circ = $self->editor->search_action_circulation(
2185 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2188 $circ = $self->editor->search_action_circulation(
2190 target_copy => $self->copy->id,
2191 stop_fines => OILS_STOP_FINES_MAX_FINES,
2192 checkin_time => undef
2197 return $self->bail_on_events($self->editor->event) unless $circ;
2199 # A user is not allowed to renew another user's items without permission
2200 unless( $circ->usr eq $self->editor->requestor->id ) {
2201 return $self->bail_on_events($self->editor->events)
2202 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2205 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2206 if $circ->renewal_remaining < 1;
2208 # -----------------------------------------------------------------
2210 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2213 $self->run_renew_permit;
2216 $self->do_checkin();
2217 return if $self->bail_out;
2219 unless( $self->permit_override ) {
2221 return if $self->bail_out;
2222 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2223 $self->remove_event('ITEM_NOT_CATALOGED');
2226 $self->override_events;
2227 return if $self->bail_out;
2230 $self->do_checkout();
2235 my( $self, $evt ) = @_;
2236 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2237 $logger->debug("circulator: removing event from list: $evt");
2238 my @events = @{$self->events};
2239 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2244 my( $self, $evt ) = @_;
2245 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2246 return grep { $_->{textcode} eq $evt } @{$self->events};
2251 sub run_renew_permit {
2256 if(!$self->legacy_script_support) {
2257 my $results = $self->run_indb_circ_test;
2258 unless($self->circ_test_success) {
2259 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2264 my $runner = $self->script_runner;
2266 $runner->load($self->circ_permit_renew);
2267 my $result = $runner->run or
2268 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2269 my $events = $result->{events};
2272 $logger->activity("ciculator: circ_permit_renew for user ".
2273 $self->patron->id." returned events: @$events") if @$events;
2275 $self->push_events(OpenILS::Event->new($_)) for @$events;
2277 $logger->debug("circulator: re-creating script runner to be safe");
2278 #$self->mk_script_runner;