1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
8 use OpenILS::Application::AppUtils;
9 my $U = "OpenILS::Application::AppUtils";
13 my $legacy_script_support = 0;
18 my $conf = OpenSRF::Utils::SettingsClient->new;
19 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
20 my @pfx = ( @pfx2, "scripts" );
22 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
23 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
24 my $d = $conf->config_value( @pfx, 'circ_duration' );
25 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
26 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
27 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
28 my $lb = $conf->config_value( @pfx2, 'script_path' );
30 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
31 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
33 $logger->error( "Missing circ script(s)" )
34 unless( $p and $c and $d and $f and $m and $pr );
36 $scripts{circ_permit_patron} = $p;
37 $scripts{circ_permit_copy} = $c;
38 $scripts{circ_duration} = $d;
39 $scripts{circ_recurring_fines}= $f;
40 $scripts{circ_max_fines} = $m;
41 $scripts{circ_permit_renew} = $pr;
43 $lb = [ $lb ] unless ref($lb);
47 "circulator: Loaded rules scripts for circ: " .
48 "circ permit patron = $p, ".
49 "circ permit copy = $c, ".
50 "circ duration = $d, ".
51 "circ recurring fines = $f, " .
52 "circ max fines = $m, ".
53 "circ renew permit = $pr. ".
55 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
60 __PACKAGE__->register_method(
61 method => "run_method",
62 api_name => "open-ils.circ.checkout.permit",
64 Determines if the given checkout can occur
65 @param authtoken The login session key
66 @param params A trailing hash of named params including
67 barcode : The copy barcode,
68 patron : The patron the checkout is occurring for,
69 renew : true or false - whether or not this is a renewal
70 @return The event that occurred during the permit check.
74 __PACKAGE__->register_method (
75 method => 'run_method',
76 api_name => 'open-ils.circ.checkout.permit.override',
77 signature => q/@see open-ils.circ.checkout.permit/,
81 __PACKAGE__->register_method(
82 method => "run_method",
83 api_name => "open-ils.circ.checkout",
86 @param authtoken The login session key
87 @param params A named hash of params including:
89 barcode If no copy is provided, the copy is retrieved via barcode
90 copyid If no copy or barcode is provide, the copy id will be use
91 patron The patron's id
92 noncat True if this is a circulation for a non-cataloted item
93 noncat_type The non-cataloged type id
94 noncat_circ_lib The location for the noncat circ.
95 precat The item has yet to be cataloged
96 dummy_title The temporary title of the pre-cataloded item
97 dummy_author The temporary authr of the pre-cataloded item
98 Default is the home org of the staff member
99 @return The SUCCESS event on success, any other event depending on the error
102 __PACKAGE__->register_method(
103 method => "run_method",
104 api_name => "open-ils.circ.checkin",
107 Generic super-method for handling all copies
108 @param authtoken The login session key
109 @param params Hash of named parameters including:
110 barcode - The copy barcode
111 force - If true, copies in bad statuses will be checked in and give good statuses
116 __PACKAGE__->register_method(
117 method => "run_method",
118 api_name => "open-ils.circ.checkin.override",
119 signature => q/@see open-ils.circ.checkin/
122 __PACKAGE__->register_method(
123 method => "run_method",
124 api_name => "open-ils.circ.renew.override",
125 signature => q/@see open-ils.circ.renew/,
129 __PACKAGE__->register_method(
130 method => "run_method",
131 api_name => "open-ils.circ.renew",
132 notes => <<" NOTES");
133 PARAMS( authtoken, circ => circ_id );
134 open-ils.circ.renew(login_session, circ_object);
135 Renews the provided circulation. login_session is the requestor of the
136 renewal and if the logged in user is not the same as circ->usr, then
137 the logged in user must have RENEW_CIRC permissions.
140 __PACKAGE__->register_method(
141 method => "run_method",
142 api_name => "open-ils.circ.checkout.full");
143 __PACKAGE__->register_method(
144 method => "run_method",
145 api_name => "open-ils.circ.checkout.full.override");
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.inspect",
151 Returns the circ matrix test result and, on success, the rule set and matrix test object
158 my( $self, $conn, $auth, $args ) = @_;
159 translate_legacy_args($args);
160 my $api = $self->api_name;
163 OpenILS::Application::Circ::Circulator->new($auth, %$args);
165 return circ_events($circulator) if $circulator->bail_out;
167 # --------------------------------------------------------------------------
168 # Go ahead and load the script runner to make sure we have all
169 # of the objects we need
170 # --------------------------------------------------------------------------
171 $circulator->is_renewal(1) if $api =~ /renew/;
172 $circulator->is_checkin(1) if $api =~ /checkin/;
173 $circulator->check_penalty_on_renew(1) if
174 $circulator->is_renewal and $U->ou_ancestor_setting_value(
175 $circulator->circ_lib, 'circ.renew.check_penalty', $circulator->editor);
177 if($legacy_script_support and not $circulator->is_checkin) {
178 $circulator->mk_script_runner();
179 $circulator->legacy_script_support(1);
180 $circulator->circ_permit_patron($scripts{circ_permit_patron});
181 $circulator->circ_permit_copy($scripts{circ_permit_copy});
182 $circulator->circ_duration($scripts{circ_duration});
183 $circulator->circ_permit_renew($scripts{circ_permit_renew});
185 $circulator->mk_env();
187 return circ_events($circulator) if $circulator->bail_out;
190 $circulator->override(1) if $api =~ /override/o;
192 if( $api =~ /checkout\.permit/ ) {
193 $circulator->do_permit();
195 } elsif( $api =~ /checkout.full/ ) {
197 # requesting a precat checkout implies that any required
198 # overrides have been performed. Go ahead and re-override.
199 $circulator->override(1) if $circulator->request_precat;
200 $circulator->do_permit();
201 unless( $circulator->bail_out ) {
202 $circulator->events([]);
203 $circulator->do_checkout();
206 } elsif( $api =~ /inspect/ ) {
207 my $data = $circulator->do_inspect();
208 $circulator->editor->rollback;
211 } elsif( $api =~ /checkout/ ) {
212 $circulator->do_checkout();
214 } elsif( $api =~ /checkin/ ) {
215 $circulator->do_checkin();
217 } elsif( $api =~ /renew/ ) {
218 $circulator->is_renewal(1);
219 $circulator->do_renew();
222 if( $circulator->bail_out ) {
225 # make sure no success event accidentally slip in
227 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
230 my @e = @{$circulator->events};
231 push( @ee, $_->{textcode} ) for @e;
232 $logger->info("circulator: bailing out with events: @ee");
234 $circulator->editor->rollback;
237 $circulator->editor->commit;
240 $circulator->script_runner->cleanup if $circulator->script_runner;
242 $conn->respond_complete(circ_events($circulator));
244 unless($circulator->bail_out) {
245 $circulator->do_hold_notify($circulator->notify_hold)
246 if $circulator->notify_hold;
247 $circulator->retarget_holds if $circulator->retarget;
253 my @e = @{$circ->events};
254 # if we have multiple events, SUCCESS should not be one of them;
255 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
256 return (@e == 1) ? $e[0] : \@e;
260 sub translate_legacy_args {
263 if( $$args{barcode} ) {
264 $$args{copy_barcode} = $$args{barcode};
265 delete $$args{barcode};
268 if( $$args{copyid} ) {
269 $$args{copy_id} = $$args{copyid};
270 delete $$args{copyid};
273 if( $$args{patronid} ) {
274 $$args{patron_id} = $$args{patronid};
275 delete $$args{patronid};
278 if( $$args{patron} and !ref($$args{patron}) ) {
279 $$args{patron_id} = $$args{patron};
280 delete $$args{patron};
284 if( $$args{noncat} ) {
285 $$args{is_noncat} = $$args{noncat};
286 delete $$args{noncat};
289 if( $$args{precat} ) {
290 $$args{is_precat} = $$args{request_precat} = $$args{precat};
291 delete $$args{precat};
297 # --------------------------------------------------------------------------
298 # This package actually manages all of the circulation logic
299 # --------------------------------------------------------------------------
300 package OpenILS::Application::Circ::Circulator;
301 use strict; use warnings;
302 use vars q/$AUTOLOAD/;
304 use OpenILS::Utils::Fieldmapper;
305 use OpenSRF::Utils::Cache;
306 use Digest::MD5 qw(md5_hex);
307 use DateTime::Format::ISO8601;
308 use OpenILS::Utils::PermitHold;
309 use OpenSRF::Utils qw/:datetime/;
310 use OpenSRF::Utils::SettingsClient;
311 use OpenILS::Application::Circ::Holds;
312 use OpenILS::Application::Circ::Transit;
313 use OpenSRF::Utils::Logger qw(:logger);
314 use OpenILS::Utils::CStoreEditor qw/:funcs/;
315 use OpenILS::Application::Circ::ScriptBuilder;
316 use OpenILS::Const qw/:const/;
318 my $holdcode = "OpenILS::Application::Circ::Holds";
319 my $transcode = "OpenILS::Application::Circ::Transit";
325 # --------------------------------------------------------------------------
326 # Add a pile of automagic getter/setter methods
327 # --------------------------------------------------------------------------
328 my @AUTOLOAD_FIELDS = qw/
343 check_penalty_on_renew
371 recurring_fines_level
384 cancelled_hold_transit
393 legacy_script_support
405 my $type = ref($self) or die "$self is not an object";
407 my $name = $AUTOLOAD;
410 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
411 $logger->error("circulator: $type: invalid autoload field: $name");
412 die "$type: invalid autoload field: $name\n"
417 *{"${type}::${name}"} = sub {
420 $s->{$name} = $v if defined $v;
424 return $self->$name($data);
429 my( $class, $auth, %args ) = @_;
430 $class = ref($class) || $class;
431 my $self = bless( {}, $class );
434 $self->editor(new_editor(xact => 1, authtoken => $auth));
436 unless( $self->editor->checkauth ) {
437 $self->bail_on_events($self->editor->event);
441 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
443 $self->$_($args{$_}) for keys %args;
446 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
448 # if this is a renewal, default to desk_renewal
449 $self->desk_renewal(1) unless
450 $self->opac_renewal or $self->phone_renewal;
452 $self->capture('') unless $self->capture;
454 unless(%user_groups) {
455 my $gps = $self->editor->retrieve_all_permission_grp_tree;
456 %user_groups = map { $_->id => $_ } @$gps;
463 # --------------------------------------------------------------------------
464 # True if we should discontinue processing
465 # --------------------------------------------------------------------------
467 my( $self, $bool ) = @_;
468 if( defined $bool ) {
469 $logger->info("circulator: BAILING OUT") if $bool;
470 $self->{bail_out} = $bool;
472 return $self->{bail_out};
477 my( $self, @evts ) = @_;
480 $logger->info("circulator: pushing event ".$e->{textcode});
481 push( @{$self->events}, $e ) unless
482 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
488 my $key = md5_hex( time() . rand() . "$$" );
489 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
490 return $self->permit_key($key);
493 sub check_permit_key {
495 my $key = $self->permit_key;
496 return 0 unless $key;
497 my $k = "oils_permit_key_$key";
498 my $one = $self->cache_handle->get_cache($k);
499 $self->cache_handle->delete_cache($k);
500 return ($one) ? 1 : 0;
505 my $e = $self->editor;
507 # --------------------------------------------------------------------------
508 # Grab the fleshed copy
509 # --------------------------------------------------------------------------
510 unless($self->is_noncat) {
514 flesh_fields => {acp => ['call_number'], acn => ['record']}
517 $copy = $e->retrieve_asset_copy(
518 [$self->copy_id, $flesh ]) or return $e->event;
520 } elsif( $self->copy_barcode ) {
522 $copy = $e->search_asset_copy(
523 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
528 $self->volume($copy->call_number);
529 $self->title($self->volume->record);
530 $self->copy->call_number($self->volume->id);
531 $self->volume->record($self->title->id);
532 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
533 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
534 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
535 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
538 # We can't renew if there is no copy
539 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
540 if $self->is_renewal;
545 return undef if $self->is_checkin;
547 # --------------------------------------------------------------------------
549 # --------------------------------------------------------------------------
551 if( $self->patron_id ) {
552 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
554 } elsif( $self->patron_barcode ) {
556 my $card = $e->search_actor_card(
557 {barcode => $self->patron_barcode})->[0] or return $e->event;
559 $patron = $e->search_actor_user(
560 {card => $card->id})->[0] or return $e->event;
563 if( my $copy = $self->copy ) {
564 my $circs = $e->search_action_circulation(
565 {target_copy => $copy->id, checkin_time => undef});
567 if( my $circ = $circs->[0] ) {
568 $patron = $e->retrieve_actor_user($circ->usr)
574 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
575 unless $self->patron($patron);
578 # --------------------------------------------------------------------------
579 # This builds the script runner environment and fetches most of the
581 # --------------------------------------------------------------------------
582 sub mk_script_runner {
588 qw/copy copy_barcode copy_id patron
589 patron_id patron_barcode volume title editor/;
591 # Translate our objects into the ScriptBuilder args hash
592 $$args{$_} = $self->$_() for @fields;
594 $args->{ignore_user_status} = 1 if $self->is_checkin;
595 $$args{fetch_patron_by_circ_copy} = 1;
596 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
598 if( my $pco = $self->pending_checkouts ) {
599 $logger->info("circulator: we were given a pending checkouts number of $pco");
600 $$args{patronItemsOut} = $pco;
603 # This fetches most of the objects we need
604 $self->script_runner(
605 OpenILS::Application::Circ::ScriptBuilder->build($args));
607 # Now we translate the ScriptBuilder objects back into self
608 $self->$_($$args{$_}) for @fields;
610 my @evts = @{$args->{_events}} if $args->{_events};
612 $logger->debug("circulator: script builder returned events: @evts") if @evts;
616 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
617 if(!$self->is_noncat and
619 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
623 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
624 return $self->bail_on_events(@e);
629 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
630 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
631 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
632 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
636 # We can't renew if there is no copy
637 return $self->bail_on_events(@evts) if
638 $self->is_renewal and !$self->copy;
640 # Set some circ-specific flags in the script environment
641 my $evt = "environment";
642 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
644 if( $self->is_noncat ) {
645 $self->script_runner->insert("$evt.isNonCat", 1);
646 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
649 if( $self->is_precat ) {
650 $self->script_runner->insert("environment.isPrecat", 1, 1);
653 $self->script_runner->add_path( $_ ) for @$script_libs;
658 # --------------------------------------------------------------------------
659 # Does the circ permit work
660 # --------------------------------------------------------------------------
664 $self->log_me("do_permit()");
666 unless( $self->editor->requestor->id == $self->patron->id ) {
667 return $self->bail_on_events($self->editor->event)
668 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
671 $self->check_captured_holds();
672 $self->do_copy_checks();
673 return if $self->bail_out;
674 $self->run_patron_permit_scripts();
675 $self->run_copy_permit_scripts()
676 unless $self->is_precat or $self->is_noncat;
677 $self->check_item_deposit_events();
678 $self->override_events() unless
679 $self->is_renewal and not $self->check_penalty_on_renew;
680 return if $self->bail_out;
682 if($self->is_precat and not $self->request_precat) {
685 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
686 return $self->bail_out(1) unless $self->is_renewal;
692 payload => $self->mk_permit_key));
695 sub check_item_deposit_events {
697 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
698 if $self->is_deposit and not $self->is_deposit_exempt;
699 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
700 if $self->is_rental and not $self->is_rental_exempt;
703 # returns true if the user is not required to pay deposits
704 sub is_deposit_exempt {
706 my $pid = (ref $self->patron->profile) ?
707 $self->patron->profile->id : $self->patron->profile;
708 my $groups = $U->ou_ancestor_setting_value(
709 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
710 for my $grp (@$groups) {
711 return 1 if $self->is_group_descendant($grp, $pid);
716 # returns true if the user is not required to pay rental fees
717 sub is_rental_exempt {
719 my $pid = (ref $self->patron->profile) ?
720 $self->patron->profile->id : $self->patron->profile;
721 my $groups = $U->ou_ancestor_setting_value(
722 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
723 for my $grp (@$groups) {
724 return 1 if $self->is_group_descendant($grp, $pid);
729 sub is_group_descendant {
730 my($self, $p_id, $c_id) = @_;
731 return 0 unless defined $p_id and defined $c_id;
732 return 1 if $c_id == $p_id;
733 while(my $grp = $user_groups{$c_id}) {
734 $c_id = $grp->parent;
735 return 0 unless defined $c_id;
736 return 1 if $c_id == $p_id;
741 sub check_captured_holds {
743 my $copy = $self->copy;
744 my $patron = $self->patron;
746 return undef unless $copy;
748 my $s = $U->copy_status($copy->status)->id;
749 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
750 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
752 # Item is on the holds shelf, make sure it's going to the right person
753 my $holds = $self->editor->search_action_hold_request(
756 current_copy => $copy->id ,
757 capture_time => { '!=' => undef },
758 cancel_time => undef,
759 fulfillment_time => undef
765 if( $holds and $$holds[0] ) {
766 return undef if $$holds[0]->usr == $patron->id;
769 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
771 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
777 my $copy = $self->copy;
780 my $stat = $U->copy_status($copy->status)->id;
782 # We cannot check out a copy if it is in-transit
783 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
784 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
787 $self->handle_claims_returned();
788 return if $self->bail_out;
790 # no claims returned circ was found, check if there is any open circ
791 unless( $self->is_renewal ) {
792 my $circs = $self->editor->search_action_circulation(
793 { target_copy => $copy->id, checkin_time => undef }
796 return $self->bail_on_events(
797 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
802 sub send_penalty_request {
804 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
805 $self->penalty_request(
807 'open-ils.penalty.patron_penalty.calculate',
809 authtoken => $self->editor->authtoken,
810 patron => $self->patron } ) );
813 sub gather_penalty_request {
815 return [] unless $self->penalty_request;
816 my $data = $self->penalty_request->recv;
818 throw $data if UNIVERSAL::isa($data,'Error');
819 $data = $data->content;
820 return $data->{fatal_penalties};
822 $logger->error("circulator: penalty request returned no data");
826 my $LEGACY_CIRC_EVENT_MAP = {
827 'actor.usr.barred' => 'PATRON_BARRED',
828 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
829 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
830 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
831 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
832 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
833 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
834 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
835 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
839 # ---------------------------------------------------------------------
840 # This pushes any patron-related events into the list but does not
841 # set bail_out for any events
842 # ---------------------------------------------------------------------
843 sub run_patron_permit_scripts {
845 my $runner = $self->script_runner;
846 my $patronid = $self->patron->id;
850 if(!$self->legacy_script_support) {
852 my $results = $self->run_indb_circ_test;
853 unless($self->circ_test_success) {
854 push(@allevents, OpenILS::Event->new(
855 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
860 $self->send_penalty_request() unless
861 $self->is_renewal and not $self->check_penalty_on_renew;
864 # --------------------------------------------------------------------- # Now run the patron permit script
865 # ---------------------------------------------------------------------
866 $runner->load($self->circ_permit_patron);
867 my $result = $runner->run or
868 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
870 my $patron_events = $result->{events};
872 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
873 [] : $self->gather_penalty_request();
875 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
878 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
880 $self->push_events(@allevents);
883 sub run_indb_circ_test {
885 return $self->matrix_test_result if $self->matrix_test_result;
887 my $dbfunc = ($self->is_renewal) ?
888 'action.item_user_renew_test' : 'action.item_user_circ_test';
890 my $results = ($self->matrix_test_result) ?
891 $self->matrix_test_result :
892 $self->editor->json_query(
895 $self->editor->requestor->ws_ou,
896 ($self->is_precat) ? undef : $self->copy->id,
902 $self->circ_test_success($U->is_true($results->[0]->{success}));
903 if($self->circ_test_success) {
904 $self->circ_matrix_test(
905 $self->editor->retrieve_config_circ_matrix_test(
906 $results->[0]->{matchpoint}
911 if($self->circ_test_success) {
912 $self->circ_matrix_ruleset(
913 $self->editor->retrieve_config_circ_matrix_ruleset([
914 $results->[0]->{matchpoint},
917 'ccmrs' => ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']
925 return $self->matrix_test_result($results);
930 $self->run_indb_circ_test;
933 circ_test_success => $self->circ_test_success,
934 failure_events => [],
938 unless($self->circ_test_success) {
939 push(@{$results->{failure_codes}},
940 $_->{fail_part}) for @{$self->matrix_test_result};
941 push(@{$results->{failure_events}},
942 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
943 for @{$self->matrix_test_result};
947 my $duration_rule = $self->circ_matrix_ruleset->duration_rule;
948 my $recurring_fine_rule = $self->circ_matrix_ruleset->recurring_fine_rule;
949 my $max_fine_rule = $self->circ_matrix_ruleset->max_fine_rule;
951 my $policy = $self->get_circ_policy(
952 $duration_rule, $recurring_fine_rule, $max_fine_rule);
954 $$results{$_} = $$policy{$_} for keys %$policy;
958 # ---------------------------------------------------------------------
959 # Loads the circ policy info for duration, recurring fine, and max
960 # fine based on the current copy
961 # ---------------------------------------------------------------------
962 sub get_circ_policy {
963 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
966 duration_rule => $duration_rule->name,
967 recurring_fine_rule => $recurring_fine_rule->name,
968 max_fine_rule => $max_fine_rule->name,
969 max_fine => $self->get_max_fine_amount($max_fine_rule),
970 fine_interval => $recurring_fine_rule->recurance_interval,
971 renewal_remaining => $duration_rule->max_renewals
974 $policy->{duration} = $duration_rule->shrt
975 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
976 $policy->{duration} = $duration_rule->normal
977 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
978 $policy->{duration} = $duration_rule->extended
979 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
981 $policy->{recurring_fine} = $recurring_fine_rule->low
982 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
983 $policy->{recurring_fine} = $recurring_fine_rule->normal
984 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
985 $policy->{recurring_fine} = $recurring_fine_rule->high
986 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
991 sub get_max_fine_amount {
993 my $max_fine_rule = shift;
994 my $max_amount = $max_fine_rule->amount;
996 # if is_percent is true then the max->amount is
997 # use as a percentage of the copy price
998 if ($U->is_true($max_fine_rule->is_percent)) {
999 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1000 $max_amount = $price * $max_fine_rule->amount / 100;
1008 sub run_copy_permit_scripts {
1010 my $copy = $self->copy || return;
1011 my $runner = $self->script_runner;
1015 if(!$self->legacy_script_support) {
1016 my $results = $self->run_indb_circ_test;
1017 unless($self->circ_test_success) {
1018 push(@allevents, OpenILS::Event->new(
1019 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})) for @$results;
1023 # ---------------------------------------------------------------------
1024 # Capture all of the copy permit events
1025 # ---------------------------------------------------------------------
1026 $runner->load($self->circ_permit_copy);
1027 my $result = $runner->run or
1028 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1029 my $copy_events = $result->{events};
1031 # ---------------------------------------------------------------------
1032 # Now collect all of the events together
1033 # ---------------------------------------------------------------------
1034 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1037 # See if this copy has an alert message
1038 my $ae = $self->check_copy_alert();
1039 push( @allevents, $ae ) if $ae;
1041 # uniquify the events
1042 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1043 @allevents = values %hash;
1046 $_->{payload} = $copy if
1047 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1050 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1052 $self->push_events(@allevents);
1056 sub check_copy_alert {
1058 return undef if $self->is_renewal;
1059 return OpenILS::Event->new(
1060 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1061 if $self->copy and $self->copy->alert_message;
1067 # --------------------------------------------------------------------------
1068 # If the call is overriding and has permissions to override every collected
1069 # event, the are cleared. Any event that the caller does not have
1070 # permission to override, will be left in the event list and bail_out will
1072 # XXX We need code in here to cancel any holds/transits on copies
1073 # that are being force-checked out
1074 # --------------------------------------------------------------------------
1075 sub override_events {
1077 my @events = @{$self->events};
1078 return unless @events;
1080 if(!$self->override) {
1081 return $self->bail_out(1)
1082 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1087 for my $e (@events) {
1088 my $tc = $e->{textcode};
1089 next if $tc eq 'SUCCESS';
1090 my $ov = "$tc.override";
1091 $logger->info("circulator: attempting to override event: $ov");
1093 return $self->bail_on_events($self->editor->event)
1094 unless( $self->editor->allowed($ov) );
1099 # --------------------------------------------------------------------------
1100 # If there is an open claimsreturn circ on the requested copy, close the
1101 # circ if overriding, otherwise bail out
1102 # --------------------------------------------------------------------------
1103 sub handle_claims_returned {
1105 my $copy = $self->copy;
1107 my $CR = $self->editor->search_action_circulation(
1109 target_copy => $copy->id,
1110 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1111 checkin_time => undef,
1115 return unless ($CR = $CR->[0]);
1119 # - If the caller has set the override flag, we will check the item in
1120 if($self->override) {
1122 $CR->checkin_time('now');
1123 $CR->checkin_lib($self->editor->requestor->ws_ou);
1124 $CR->checkin_staff($self->editor->requestor->id);
1126 $evt = $self->editor->event
1127 unless $self->editor->update_action_circulation($CR);
1130 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1133 $self->bail_on_events($evt) if $evt;
1138 # --------------------------------------------------------------------------
1139 # This performs the checkout
1140 # --------------------------------------------------------------------------
1144 $self->log_me("do_checkout()");
1146 # make sure perms are good if this isn't a renewal
1147 unless( $self->is_renewal ) {
1148 return $self->bail_on_events($self->editor->event)
1149 unless( $self->editor->allowed('COPY_CHECKOUT') );
1152 # verify the permit key
1153 unless( $self->check_permit_key ) {
1154 if( $self->permit_override ) {
1155 return $self->bail_on_events($self->editor->event)
1156 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1158 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1162 # if this is a non-cataloged circ, build the circ and finish
1163 if( $self->is_noncat ) {
1164 $self->checkout_noncat;
1166 OpenILS::Event->new('SUCCESS',
1167 payload => { noncat_circ => $self->circ }));
1171 if( $self->is_precat ) {
1172 #$self->script_runner->insert("environment.isPrecat", 1, 1)
1173 $self->make_precat_copy;
1174 return if $self->bail_out;
1176 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1177 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1180 $self->do_copy_checks;
1181 return if $self->bail_out;
1183 $self->run_checkout_scripts();
1184 return if $self->bail_out;
1186 $self->build_checkout_circ_object();
1187 return if $self->bail_out;
1189 $self->apply_modified_due_date();
1190 return if $self->bail_out;
1192 return $self->bail_on_events($self->editor->event)
1193 unless $self->editor->create_action_circulation($self->circ);
1195 # refresh the circ to force local time zone for now
1196 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1198 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1200 return if $self->bail_out;
1202 $self->apply_deposit_fee();
1203 return if $self->bail_out;
1205 $self->handle_checkout_holds();
1206 return if $self->bail_out;
1208 # ------------------------------------------------------------------------------
1209 # Update the patron penalty info in the DB. Run it for permit-overrides or
1210 # renewals since both of those cases do not require the penalty server to
1211 # run during the permit phase of the checkout
1212 # ------------------------------------------------------------------------------
1213 if( $self->permit_override or $self->is_renewal ) {
1214 $U->update_patron_penalties(
1215 authtoken => $self->editor->authtoken,
1216 patron => $self->patron,
1221 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1223 OpenILS::Event->new('SUCCESS',
1225 copy => $U->unflesh_copy($self->copy),
1226 circ => $self->circ,
1228 holds_fulfilled => $self->fulfilled_holds,
1229 deposit_billing => $self->deposit_billing,
1230 rental_billing => $self->rental_billing
1236 sub apply_deposit_fee {
1238 my $copy = $self->copy;
1240 ($self->is_deposit and not $self->is_deposit_exempt) or
1241 ($self->is_rental and not $self->is_rental_exempt);
1243 my $bill = Fieldmapper::money::billing->new;
1244 my $amount = $copy->deposit_amount;
1247 if($self->is_deposit) {
1248 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1249 $self->deposit_billing($bill);
1251 $billing_type = OILS_BILLING_TYPE_RENTAL;
1252 $self->rental_billing($bill);
1255 $bill->xact($self->circ->id);
1256 $bill->amount($amount);
1257 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1258 $bill->billing_type($billing_type);
1259 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1261 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1266 my $copy = $self->copy;
1268 my $stat = $copy->status if ref $copy->status;
1269 my $loc = $copy->location if ref $copy->location;
1270 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1272 $copy->status($stat->id) if $stat;
1273 $copy->location($loc->id) if $loc;
1274 $copy->circ_lib($circ_lib->id) if $circ_lib;
1275 $copy->editor($self->editor->requestor->id);
1276 $copy->edit_date('now');
1277 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1279 return $self->bail_on_events($self->editor->event)
1280 unless $self->editor->update_asset_copy($self->copy);
1282 $copy->status($U->copy_status($copy->status));
1283 $copy->location($loc) if $loc;
1284 $copy->circ_lib($circ_lib) if $circ_lib;
1288 sub bail_on_events {
1289 my( $self, @evts ) = @_;
1290 $self->push_events(@evts);
1294 sub handle_checkout_holds {
1297 my $copy = $self->copy;
1298 my $patron = $self->patron;
1300 my $holds = $self->editor->search_action_hold_request(
1302 current_copy => $copy->id ,
1303 cancel_time => undef,
1304 fulfillment_time => undef
1310 # XXX We should only fulfill one hold here...
1311 # XXX If a hold was transited to the user who is checking out
1312 # the item, we need to make sure that hold is what's grabbed
1315 # for now, just sort by id to get what should be the oldest hold
1316 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1317 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1318 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1321 my $hold = $myholds[0];
1323 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1325 # if the hold was never officially captured, capture it.
1326 $hold->capture_time('now') unless $hold->capture_time;
1328 # just make sure it's set correctly
1329 $hold->current_copy($copy->id);
1331 $hold->fulfillment_time('now');
1332 $hold->fulfillment_staff($self->editor->requestor->id);
1333 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1335 return $self->bail_on_events($self->editor->event)
1336 unless $self->editor->update_action_hold_request($hold);
1338 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1340 push( @fulfilled, $hold->id );
1343 # If there are any holds placed for other users that point to this copy,
1344 # then we need to un-target those holds so the targeter can pick a new copy
1347 $logger->info("circulator: un-targeting hold ".$_->id.
1348 " because copy ".$copy->id." is getting checked out");
1350 # - make the targeter process this hold at next run
1351 $_->clear_prev_check_time;
1353 # - clear out the targetted copy
1354 $_->clear_current_copy;
1355 $_->clear_capture_time;
1357 return $self->bail_on_event($self->editor->event)
1358 unless $self->editor->update_action_hold_request($_);
1362 $self->fulfilled_holds(\@fulfilled);
1367 sub run_checkout_scripts {
1371 my $runner = $self->script_runner;
1380 if(!$self->legacy_script_support) {
1381 $self->run_indb_circ_test();
1382 $duration = $self->circ_matrix_ruleset->duration_rule;
1383 $recurring = $self->circ_matrix_ruleset->recurring_fine_rule;
1384 $max_fine = $self->circ_matrix_ruleset->max_fine_rule;
1388 $runner->load($self->circ_duration);
1390 my $result = $runner->run or
1391 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1393 $duration_name = $result->{durationRule};
1394 $recurring_name = $result->{recurringFinesRule};
1395 $max_fine_name = $result->{maxFine};
1398 $duration_name = $duration->name if $duration;
1399 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1402 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1403 return $self->bail_on_events($evt) if $evt;
1405 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1406 return $self->bail_on_events($evt) if $evt;
1408 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1409 return $self->bail_on_events($evt) if $evt;
1414 # The item circulates with an unlimited duration
1420 $self->duration_rule($duration);
1421 $self->recurring_fines_rule($recurring);
1422 $self->max_fine_rule($max_fine);
1426 sub build_checkout_circ_object {
1429 my $circ = Fieldmapper::action::circulation->new;
1430 my $duration = $self->duration_rule;
1431 my $max = $self->max_fine_rule;
1432 my $recurring = $self->recurring_fines_rule;
1433 my $copy = $self->copy;
1434 my $patron = $self->patron;
1438 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1440 my $dname = $duration->name;
1441 my $mname = $max->name;
1442 my $rname = $recurring->name;
1444 $logger->debug("circulator: building circulation ".
1445 "with duration=$dname, maxfine=$mname, recurring=$rname");
1447 $circ->duration($policy->{duration});
1448 $circ->recuring_fine($policy->{recurring_fine});
1449 $circ->duration_rule($duration->name);
1450 $circ->recuring_fine_rule($recurring->name);
1451 $circ->max_fine_rule($max->name);
1452 $circ->max_fine($policy->{max_fine});
1453 $circ->fine_interval($recurring->recurance_interval);
1454 $circ->renewal_remaining($duration->max_renewals);
1458 $logger->info("circulator: copy found with an unlimited circ duration");
1459 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1460 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1461 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1462 $circ->renewal_remaining(0);
1465 $circ->target_copy( $copy->id );
1466 $circ->usr( $patron->id );
1467 $circ->circ_lib( $self->circ_lib );
1469 if( $self->is_renewal ) {
1470 $circ->opac_renewal('t') if $self->opac_renewal;
1471 $circ->phone_renewal('t') if $self->phone_renewal;
1472 $circ->desk_renewal('t') if $self->desk_renewal;
1473 $circ->renewal_remaining($self->renewal_remaining);
1474 $circ->circ_staff($self->editor->requestor->id);
1478 # if the user provided an overiding checkout time,
1479 # (e.g. the checkout really happened several hours ago), then
1480 # we apply that here. Does this need a perm??
1481 $circ->xact_start(clense_ISO8601($self->checkout_time))
1482 if $self->checkout_time;
1484 # if a patron is renewing, 'requestor' will be the patron
1485 $circ->circ_staff($self->editor->requestor->id);
1486 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1492 sub apply_modified_due_date {
1494 my $circ = $self->circ;
1495 my $copy = $self->copy;
1497 if( $self->due_date ) {
1499 return $self->bail_on_events($self->editor->event)
1500 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1502 $circ->due_date(clense_ISO8601($self->due_date));
1506 # if the due_date lands on a day when the location is closed
1507 return unless $copy and $circ->due_date;
1509 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1511 # due-date overlap should be determined by the location the item
1512 # is checked out from, not the owning or circ lib of the item
1513 my $org = $self->editor->requestor->ws_ou;
1515 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1516 " with an item due date of ".$circ->due_date );
1518 my $dateinfo = $U->storagereq(
1519 'open-ils.storage.actor.org_unit.closed_date.overlap',
1520 $org, $circ->due_date );
1523 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1524 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1526 # XXX make the behavior more dynamic
1527 # for now, we just push the due date to after the close date
1528 $circ->due_date($dateinfo->{end});
1535 sub create_due_date {
1536 my( $self, $duration ) = @_;
1537 # if there is a raw time component (e.g. from postgres),
1538 # turn it into an interval that interval_to_seconds can parse
1539 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1540 my ($sec,$min,$hour,$mday,$mon,$year) =
1541 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1542 $year += 1900; $mon += 1;
1543 my $due_date = sprintf(
1544 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1545 $year, $mon, $mday, $hour, $min, $sec);
1551 sub make_precat_copy {
1553 my $copy = $self->copy;
1556 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1558 $copy->editor($self->editor->requestor->id);
1559 $copy->edit_date('now');
1560 $copy->dummy_title($self->dummy_title);
1561 $copy->dummy_author($self->dummy_author);
1563 $self->update_copy();
1567 $logger->info("circulator: Creating a new precataloged ".
1568 "copy in checkout with barcode " . $self->copy_barcode);
1570 $copy = Fieldmapper::asset::copy->new;
1571 $copy->circ_lib($self->circ_lib);
1572 $copy->creator($self->editor->requestor->id);
1573 $copy->editor($self->editor->requestor->id);
1574 $copy->barcode($self->copy_barcode);
1575 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1576 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1577 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1579 $copy->dummy_title($self->dummy_title || "");
1580 $copy->dummy_author($self->dummy_author || "");
1582 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1584 $self->push_events($self->editor->event);
1588 # this is a little bit of a hack, but we need to
1589 # get the copy into the script runner
1590 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1594 sub checkout_noncat {
1600 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1601 my $count = $self->noncat_count || 1;
1602 my $cotime = clense_ISO8601($self->checkout_time) || "";
1604 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1608 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1609 $self->editor->requestor->id,
1617 $self->push_events($evt);
1628 $self->log_me("do_checkin()");
1631 return $self->bail_on_events(
1632 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1635 if( $self->checkin_check_holds_shelf() ) {
1636 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1637 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1638 $self->checkin_flesh_events;
1642 unless( $self->is_renewal ) {
1643 return $self->bail_on_events($self->editor->event)
1644 unless $self->editor->allowed('COPY_CHECKIN');
1647 $self->push_events($self->check_copy_alert());
1648 $self->push_events($self->check_checkin_copy_status());
1650 # the renew code will have already found our circulation object
1651 unless( $self->is_renewal and $self->circ ) {
1652 my $circs = $self->editor->search_action_circulation(
1653 { target_copy => $self->copy->id, checkin_time => undef });
1654 $self->circ($$circs[0]);
1656 # for now, just warn if there are multiple open circs on a copy
1657 $logger->warn("circulator: we have ".scalar(@$circs).
1658 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1661 # if the circ is marked as 'claims returned', add the event to the list
1662 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1663 if ($self->circ and $self->circ->stop_fines
1664 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1666 $self->check_circ_deposit();
1668 # handle the overridable events
1669 $self->override_events unless $self->is_renewal;
1670 return if $self->bail_out;
1674 $self->editor->search_action_transit_copy(
1675 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1679 $self->checkin_handle_circ;
1680 return if $self->bail_out;
1681 $self->checkin_changed(1);
1683 } elsif( $self->transit ) {
1684 my $hold_transit = $self->process_received_transit;
1685 $self->checkin_changed(1);
1687 if( $self->bail_out ) {
1688 $self->checkin_flesh_events;
1692 if( my $e = $self->check_checkin_copy_status() ) {
1693 # If the original copy status is special, alert the caller
1694 my $ev = $self->events;
1695 $self->events([$e]);
1696 $self->override_events;
1697 return if $self->bail_out;
1701 if( $hold_transit or
1702 $U->copy_status($self->copy->status)->id
1703 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1706 if( $hold_transit ) {
1707 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1709 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1714 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1716 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1717 $self->reshelve_copy(1);
1718 $self->cancelled_hold_transit(1);
1719 $self->notify_hold(0); # don't notify for cancelled holds
1720 return if $self->bail_out;
1724 # hold transited to correct location
1725 $self->checkin_flesh_events;
1730 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1732 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1733 " that is in-transit, but there is no transit.. repairing");
1734 $self->reshelve_copy(1);
1735 return if $self->bail_out;
1738 if( $self->is_renewal ) {
1739 $self->push_events(OpenILS::Event->new('SUCCESS'));
1743 # ------------------------------------------------------------------------------
1744 # Circulations and transits are now closed where necessary. Now go on to see if
1745 # this copy can fulfill a hold or needs to be routed to a different location
1746 # ------------------------------------------------------------------------------
1748 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1750 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1751 return if $self->bail_out;
1753 unless($needed_for_hold) {
1754 my $circ_lib = (ref $self->copy->circ_lib) ?
1755 $self->copy->circ_lib->id : $self->copy->circ_lib;
1757 if( $self->remote_hold ) {
1758 $circ_lib = $self->remote_hold->pickup_lib;
1759 $logger->warn("circulator: Copy ".$self->copy->barcode.
1760 " is on a remote hold's shelf, sending to $circ_lib");
1763 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1765 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1767 $self->checkin_handle_precat();
1768 return if $self->bail_out;
1772 my $bc = $self->copy->barcode;
1773 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1774 $self->checkin_build_copy_transit($circ_lib);
1775 return if $self->bail_out;
1776 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1781 $self->reshelve_copy;
1782 return if $self->bail_out;
1784 unless($self->checkin_changed) {
1786 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1787 my $stat = $U->copy_status($self->copy->status)->id;
1789 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1790 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1791 $self->bail_out(1); # no need to commit anything
1795 $self->push_events(OpenILS::Event->new('SUCCESS'))
1796 unless @{$self->events};
1800 # ------------------------------------------------------------------------------
1801 # Update the patron penalty info in the DB
1802 # ------------------------------------------------------------------------------
1803 $U->update_patron_penalties(
1804 authtoken => $self->editor->authtoken,
1805 patron => $self->patron,
1806 background => 1 ) if $self->is_checkin;
1808 $self->checkin_flesh_events;
1812 # if a deposit was payed for this item, push the event
1813 sub check_circ_deposit {
1815 return unless $self->circ;
1816 my $deposit = $self->editor->search_money_billing(
1817 { billing_type => OILS_BILLING_TYPE_DEPOSIT,
1818 xact => $self->circ->id,
1820 }, {idlist => 1})->[0];
1822 $self->push_events(OpenILS::Event->new(
1823 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1828 my $force = $self->force || shift;
1829 my $copy = $self->copy;
1831 my $stat = $U->copy_status($copy->status)->id;
1834 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1835 $stat != OILS_COPY_STATUS_CATALOGING and
1836 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1837 $stat != OILS_COPY_STATUS_RESHELVING )) {
1839 $copy->status( OILS_COPY_STATUS_RESHELVING );
1841 $self->checkin_changed(1);
1846 # Returns true if the item is at the current location
1847 # because it was transited there for a hold and the
1848 # hold has not been fulfilled
1849 sub checkin_check_holds_shelf {
1851 return 0 unless $self->copy;
1854 $U->copy_status($self->copy->status)->id ==
1855 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1857 # find the hold that put us on the holds shelf
1858 my $holds = $self->editor->search_action_hold_request(
1860 current_copy => $self->copy->id,
1861 capture_time => { '!=' => undef },
1862 fulfillment_time => undef,
1863 cancel_time => undef,
1868 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1869 $self->reshelve_copy(1);
1873 my $hold = $$holds[0];
1875 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1876 $hold->id. "] for copy ".$self->copy->barcode);
1878 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1879 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1883 $logger->info("circulator: hold is not for here..");
1884 $self->remote_hold($hold);
1889 sub checkin_handle_precat {
1891 my $copy = $self->copy;
1893 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1894 $copy->status(OILS_COPY_STATUS_CATALOGING);
1895 $self->update_copy();
1896 $self->checkin_changed(1);
1897 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1902 sub checkin_build_copy_transit {
1905 my $copy = $self->copy;
1906 my $transit = Fieldmapper::action::transit_copy->new;
1908 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1909 $logger->info("circulator: transiting copy to $dest");
1911 $transit->source($self->editor->requestor->ws_ou);
1912 $transit->dest($dest);
1913 $transit->target_copy($copy->id);
1914 $transit->source_send_time('now');
1915 $transit->copy_status( $U->copy_status($copy->status)->id );
1917 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1919 return $self->bail_on_events($self->editor->event)
1920 unless $self->editor->create_action_transit_copy($transit);
1922 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1924 $self->checkin_changed(1);
1928 # returns true if the item was used (or may potentially be used
1929 # in subsequent calls) to capture a hold.
1930 sub attempt_checkin_hold_capture {
1932 my $copy = $self->copy;
1934 # we've been explicitly told not to capture any holds
1935 return 0 if $self->capture eq 'nocapture';
1937 # See if this copy can fulfill any holds
1938 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1939 $self->editor, $copy, $self->editor->requestor );
1942 $logger->debug("circulator: no potential permitted".
1943 "holds found for copy ".$copy->barcode);
1947 if($self->capture ne 'capture') {
1948 # see if this item is in a hold-capture-delay location
1949 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
1950 if($U->is_true($location->hold_verify)) {
1951 $self->bail_on_events(
1952 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
1957 $self->retarget($retarget);
1959 $logger->info("circulator: found permitted hold ".
1960 $hold->id . " for copy, capturing...");
1962 $hold->current_copy($copy->id);
1963 $hold->capture_time('now');
1965 # prevent DB errors caused by fetching
1966 # holds from storage, and updating through cstore
1967 $hold->clear_fulfillment_time;
1968 $hold->clear_fulfillment_staff;
1969 $hold->clear_fulfillment_lib;
1970 $hold->clear_expire_time;
1971 $hold->clear_cancel_time;
1972 $hold->clear_prev_check_time unless $hold->prev_check_time;
1974 $self->bail_on_events($self->editor->event)
1975 unless $self->editor->update_action_hold_request($hold);
1977 $self->checkin_changed(1);
1979 return 0 if $self->bail_out;
1981 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1983 # This hold was captured in the correct location
1984 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1985 $self->push_events(OpenILS::Event->new('SUCCESS'));
1987 #$self->do_hold_notify($hold->id);
1988 $self->notify_hold($hold->id);
1992 # Hold needs to be picked up elsewhere. Build a hold
1993 # transit and route the item.
1994 $self->checkin_build_hold_transit();
1995 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1996 return 0 if $self->bail_out;
1997 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2000 # make sure we save the copy status
2005 sub do_hold_notify {
2006 my( $self, $holdid ) = @_;
2008 $logger->info("circulator: running delayed hold notify process");
2010 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2011 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2013 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2014 hold_id => $holdid, requestor => $self->editor->requestor);
2016 $logger->debug("circulator: built hold notifier");
2018 if(!$notifier->event) {
2020 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
2022 my $stat = $notifier->send_email_notify;
2023 if( $stat == '1' ) {
2024 $logger->info("ciculator: hold notify succeeded for hold $holdid");
2028 $logger->warn("ciculator: * hold notify failed for hold $holdid");
2031 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
2035 sub retarget_holds {
2036 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
2037 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2038 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
2039 # no reason to wait for the return value
2043 sub checkin_build_hold_transit {
2046 my $copy = $self->copy;
2047 my $hold = $self->hold;
2048 my $trans = Fieldmapper::action::hold_transit_copy->new;
2050 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2052 $trans->hold($hold->id);
2053 $trans->source($self->editor->requestor->ws_ou);
2054 $trans->dest($hold->pickup_lib);
2055 $trans->source_send_time("now");
2056 $trans->target_copy($copy->id);
2058 # when the copy gets to its destination, it will recover
2059 # this status - put it onto the holds shelf
2060 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2062 return $self->bail_on_events($self->editor->event)
2063 unless $self->editor->create_action_hold_transit_copy($trans);
2068 sub process_received_transit {
2070 my $copy = $self->copy;
2071 my $copyid = $self->copy->id;
2073 my $status_name = $U->copy_status($copy->status)->name;
2074 $logger->debug("circulator: attempting transit receive on ".
2075 "copy $copyid. Copy status is $status_name");
2077 my $transit = $self->transit;
2079 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2080 # - this item is in-transit to a different location
2082 my $tid = $transit->id;
2083 my $loc = $self->editor->requestor->ws_ou;
2084 my $dest = $transit->dest;
2086 $logger->info("circulator: Fowarding transit on copy which is destined ".
2087 "for a different location. transit=$tid, copy=$copyid, current ".
2088 "location=$loc, destination location=$dest");
2090 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2092 # grab the associated hold object if available
2093 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2094 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2096 return $self->bail_on_events($evt);
2099 # The transit is received, set the receive time
2100 $transit->dest_recv_time('now');
2101 $self->bail_on_events($self->editor->event)
2102 unless $self->editor->update_action_transit_copy($transit);
2104 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2106 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2107 $copy->status( $transit->copy_status );
2108 $self->update_copy();
2109 return if $self->bail_out;
2113 #$self->do_hold_notify($hold_transit->hold);
2114 $self->notify_hold($hold_transit->hold);
2119 OpenILS::Event->new(
2122 payload => { transit => $transit, holdtransit => $hold_transit } ));
2124 return $hold_transit;
2128 sub checkin_handle_circ {
2132 my $circ = $self->circ;
2133 my $copy = $self->copy;
2137 # backdate the circ if necessary
2138 if($self->backdate) {
2139 $self->checkin_handle_backdate;
2140 return if $self->bail_out;
2143 if(!$circ->stop_fines) {
2144 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2145 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2146 $circ->stop_fines_time('now') unless $self->backdate;
2147 $circ->stop_fines_time($self->backdate) if $self->backdate;
2150 # see if there are any fines owed on this circ. if not, close it
2151 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2152 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2154 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2156 # Set the checkin vars since we have the item
2157 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2159 $circ->checkin_staff($self->editor->requestor->id);
2160 $circ->checkin_lib($self->editor->requestor->ws_ou);
2162 my $circ_lib = (ref $self->copy->circ_lib) ?
2163 $self->copy->circ_lib->id : $self->copy->circ_lib;
2164 my $stat = $U->copy_status($self->copy->status)->id;
2166 # If the item is lost/missing and it needs to be sent home, don't
2167 # reshelve the copy, leave it lost/missing so the recipient will know
2168 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
2169 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
2170 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2173 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2177 return $self->bail_on_events($self->editor->event)
2178 unless $self->editor->update_action_circulation($circ);
2182 sub checkin_handle_backdate {
2185 my $bd = $self->backdate;
2187 # ------------------------------------------------------------------
2188 # clean up the backdate for date comparison
2189 # we want any bills created on or after the backdate
2190 # ------------------------------------------------------------------
2191 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2192 #$bd = "${bd}T23:59:59";
2194 my $bills = $self->editor->search_money_billing(
2196 billing_ts => { '>=' => $bd },
2197 xact => $self->circ->id,
2198 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
2202 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2204 for my $bill (@$bills) {
2205 unless( $U->is_true($bill->voided) ) {
2206 $logger->info("backdate voiding bill ".$bill->id);
2208 $bill->void_time('now');
2209 $bill->voider($self->editor->requestor->id);
2210 my $n = $bill->note || "";
2211 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2213 $self->bail_on_events($self->editor->event)
2214 unless $self->editor->update_money_billing($bill);
2222 sub find_patron_from_copy {
2224 my $circs = $self->editor->search_action_circulation(
2225 { target_copy => $self->copy->id, checkin_time => undef });
2226 my $circ = $circs->[0];
2227 return unless $circ;
2228 my $u = $self->editor->retrieve_actor_user($circ->usr)
2229 or return $self->bail_on_events($self->editor->event);
2233 sub check_checkin_copy_status {
2235 my $copy = $self->copy;
2241 my $status = $U->copy_status($copy->status)->id;
2244 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2245 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2246 $status == OILS_COPY_STATUS_IN_PROCESS ||
2247 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2248 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2249 $status == OILS_COPY_STATUS_CATALOGING ||
2250 $status == OILS_COPY_STATUS_RESHELVING );
2252 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2253 if( $status == OILS_COPY_STATUS_LOST );
2255 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2256 if( $status == OILS_COPY_STATUS_MISSING );
2258 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2263 # --------------------------------------------------------------------------
2264 # On checkin, we need to return as many relevant objects as we can
2265 # --------------------------------------------------------------------------
2266 sub checkin_flesh_events {
2269 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2270 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2271 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2275 for my $evt (@{$self->events}) {
2278 $payload->{copy} = $U->unflesh_copy($self->copy);
2279 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2280 $payload->{circ} = $self->circ;
2281 $payload->{transit} = $self->transit;
2282 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2284 # $self->hold may or may not have been replaced with a
2285 # valid hold after processing a cancelled hold
2286 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2288 $evt->{payload} = $payload;
2293 my( $self, $msg ) = @_;
2294 my $bc = ($self->copy) ? $self->copy->barcode :
2297 my $usr = ($self->patron) ? $self->patron->id : "";
2298 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2299 ", recipient=$usr, copy=$bc");
2305 $self->log_me("do_renew()");
2306 $self->is_renewal(1);
2308 # Make sure there is an open circ to renew that is not
2309 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2310 my $circ = $self->editor->search_action_circulation(
2311 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2314 $circ = $self->editor->search_action_circulation(
2316 target_copy => $self->copy->id,
2317 stop_fines => OILS_STOP_FINES_MAX_FINES,
2318 checkin_time => undef
2323 return $self->bail_on_events($self->editor->event) unless $circ;
2325 # A user is not allowed to renew another user's items without permission
2326 unless( $circ->usr eq $self->editor->requestor->id ) {
2327 return $self->bail_on_events($self->editor->events)
2328 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2331 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2332 if $circ->renewal_remaining < 1;
2334 # -----------------------------------------------------------------
2336 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2339 $self->run_renew_permit;
2342 $self->do_checkin();
2343 return if $self->bail_out;
2345 unless( $self->permit_override ) {
2347 return if $self->bail_out;
2348 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2349 $self->remove_event('ITEM_NOT_CATALOGED');
2352 $self->override_events;
2353 return if $self->bail_out;
2356 $self->do_checkout();
2361 my( $self, $evt ) = @_;
2362 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2363 $logger->debug("circulator: removing event from list: $evt");
2364 my @events = @{$self->events};
2365 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2370 my( $self, $evt ) = @_;
2371 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2372 return grep { $_->{textcode} eq $evt } @{$self->events};
2377 sub run_renew_permit {
2382 if(!$self->legacy_script_support) {
2383 my $results = $self->run_indb_circ_test;
2384 unless($self->circ_test_success) {
2385 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2390 my $runner = $self->script_runner;
2392 $runner->load($self->circ_permit_renew);
2393 my $result = $runner->run or
2394 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2395 my $events = $result->{events};
2398 $logger->activity("ciculator: circ_permit_renew for user ".
2399 $self->patron->id." returned events: @$events") if @$events;
2401 $self->push_events(OpenILS::Event->new($_)) for @$events;
2403 $logger->debug("circulator: re-creating script runner to be safe");
2404 #$self->mk_script_runner;