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" );
21 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
22 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
24 my $lb = $conf->config_value( @pfx2, 'script_path' );
25 $lb = [ $lb ] unless ref($lb);
28 return unless $legacy_script_support;
30 my @pfx = ( @pfx2, "scripts" );
31 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
32 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
33 my $d = $conf->config_value( @pfx, 'circ_duration' );
34 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
35 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
36 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
38 $logger->error( "Missing circ script(s)" )
39 unless( $p and $c and $d and $f and $m and $pr );
41 $scripts{circ_permit_patron} = $p;
42 $scripts{circ_permit_copy} = $c;
43 $scripts{circ_duration} = $d;
44 $scripts{circ_recurring_fines}= $f;
45 $scripts{circ_max_fines} = $m;
46 $scripts{circ_permit_renew} = $pr;
49 "circulator: Loaded rules scripts for circ: " .
50 "circ permit patron = $p, ".
51 "circ permit copy = $c, ".
52 "circ duration = $d, ".
53 "circ recurring fines = $f, " .
54 "circ max fines = $m, ".
55 "circ renew permit = $pr. ".
57 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
62 __PACKAGE__->register_method(
63 method => "run_method",
64 api_name => "open-ils.circ.checkout.permit",
66 Determines if the given checkout can occur
67 @param authtoken The login session key
68 @param params A trailing hash of named params including
69 barcode : The copy barcode,
70 patron : The patron the checkout is occurring for,
71 renew : true or false - whether or not this is a renewal
72 @return The event that occurred during the permit check.
76 __PACKAGE__->register_method (
77 method => 'run_method',
78 api_name => 'open-ils.circ.checkout.permit.override',
79 signature => q/@see open-ils.circ.checkout.permit/,
83 __PACKAGE__->register_method(
84 method => "run_method",
85 api_name => "open-ils.circ.checkout",
88 @param authtoken The login session key
89 @param params A named hash of params including:
91 barcode If no copy is provided, the copy is retrieved via barcode
92 copyid If no copy or barcode is provide, the copy id will be use
93 patron The patron's id
94 noncat True if this is a circulation for a non-cataloted item
95 noncat_type The non-cataloged type id
96 noncat_circ_lib The location for the noncat circ.
97 precat The item has yet to be cataloged
98 dummy_title The temporary title of the pre-cataloded item
99 dummy_author The temporary authr of the pre-cataloded item
100 Default is the home org of the staff member
101 @return The SUCCESS event on success, any other event depending on the error
104 __PACKAGE__->register_method(
105 method => "run_method",
106 api_name => "open-ils.circ.checkin",
109 Generic super-method for handling all copies
110 @param authtoken The login session key
111 @param params Hash of named parameters including:
112 barcode - The copy barcode
113 force - If true, copies in bad statuses will be checked in and give good statuses
114 noop - don't capture holds or put items into transit
115 void_overdues - void all overdues for the circulation (aka amnesty)
120 __PACKAGE__->register_method(
121 method => "run_method",
122 api_name => "open-ils.circ.checkin.override",
123 signature => q/@see open-ils.circ.checkin/
126 __PACKAGE__->register_method(
127 method => "run_method",
128 api_name => "open-ils.circ.renew.override",
129 signature => q/@see open-ils.circ.renew/,
133 __PACKAGE__->register_method(
134 method => "run_method",
135 api_name => "open-ils.circ.renew",
136 notes => <<" NOTES");
137 PARAMS( authtoken, circ => circ_id );
138 open-ils.circ.renew(login_session, circ_object);
139 Renews the provided circulation. login_session is the requestor of the
140 renewal and if the logged in user is not the same as circ->usr, then
141 the logged in user must have RENEW_CIRC permissions.
144 __PACKAGE__->register_method(
145 method => "run_method",
146 api_name => "open-ils.circ.checkout.full");
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.full.override");
151 __PACKAGE__->register_method(
152 method => "run_method",
153 api_name => "open-ils.circ.checkout.inspect",
155 Returns the circ matrix test result and, on success, the rule set and matrix test object
162 my( $self, $conn, $auth, $args ) = @_;
163 translate_legacy_args($args);
164 my $api = $self->api_name;
167 OpenILS::Application::Circ::Circulator->new($auth, %$args);
169 return circ_events($circulator) if $circulator->bail_out;
171 # --------------------------------------------------------------------------
172 # Go ahead and load the script runner to make sure we have all
173 # of the objects we need
174 # --------------------------------------------------------------------------
175 $circulator->is_renewal(1) if $api =~ /renew/;
176 $circulator->is_checkin(1) if $api =~ /checkin/;
178 if($legacy_script_support and not $circulator->is_checkin) {
179 $circulator->mk_script_runner();
180 $circulator->legacy_script_support(1);
181 $circulator->circ_permit_patron($scripts{circ_permit_patron});
182 $circulator->circ_permit_copy($scripts{circ_permit_copy});
183 $circulator->circ_duration($scripts{circ_duration});
184 $circulator->circ_permit_renew($scripts{circ_permit_renew});
186 $circulator->mk_env();
188 return circ_events($circulator) if $circulator->bail_out;
191 $circulator->override(1) if $api =~ /override/o;
193 if( $api =~ /checkout\.permit/ ) {
194 $circulator->do_permit();
196 } elsif( $api =~ /checkout.full/ ) {
198 # requesting a precat checkout implies that any required
199 # overrides have been performed. Go ahead and re-override.
200 $circulator->override(1) if $circulator->request_precat;
201 $circulator->do_permit();
202 $circulator->is_checkout(1);
203 unless( $circulator->bail_out ) {
204 $circulator->events([]);
205 $circulator->do_checkout();
208 } elsif( $api =~ /inspect/ ) {
209 my $data = $circulator->do_inspect();
210 $circulator->editor->rollback;
213 } elsif( $api =~ /checkout/ ) {
214 $circulator->is_checkout(1);
215 $circulator->do_checkout();
217 } elsif( $api =~ /checkin/ ) {
218 $circulator->do_checkin();
220 } elsif( $api =~ /renew/ ) {
221 $circulator->is_renewal(1);
222 $circulator->do_renew();
225 if( $circulator->bail_out ) {
228 # make sure no success event accidentally slip in
230 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
233 my @e = @{$circulator->events};
234 push( @ee, $_->{textcode} ) for @e;
235 $logger->info("circulator: bailing out with events: @ee");
237 $circulator->editor->rollback;
240 $circulator->editor->commit;
243 $circulator->script_runner->cleanup if $circulator->script_runner;
245 $conn->respond_complete(circ_events($circulator));
247 unless($circulator->bail_out) {
248 $circulator->do_hold_notify($circulator->notify_hold)
249 if $circulator->notify_hold;
250 $circulator->retarget_holds if $circulator->retarget;
251 $circulator->append_reading_list;
252 $circulator->make_trigger_events;
258 my @e = @{$circ->events};
259 # if we have multiple events, SUCCESS should not be one of them;
260 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
261 return (@e == 1) ? $e[0] : \@e;
265 sub translate_legacy_args {
268 if( $$args{barcode} ) {
269 $$args{copy_barcode} = $$args{barcode};
270 delete $$args{barcode};
273 if( $$args{copyid} ) {
274 $$args{copy_id} = $$args{copyid};
275 delete $$args{copyid};
278 if( $$args{patronid} ) {
279 $$args{patron_id} = $$args{patronid};
280 delete $$args{patronid};
283 if( $$args{patron} and !ref($$args{patron}) ) {
284 $$args{patron_id} = $$args{patron};
285 delete $$args{patron};
289 if( $$args{noncat} ) {
290 $$args{is_noncat} = $$args{noncat};
291 delete $$args{noncat};
294 if( $$args{precat} ) {
295 $$args{is_precat} = $$args{request_precat} = $$args{precat};
296 delete $$args{precat};
302 # --------------------------------------------------------------------------
303 # This package actually manages all of the circulation logic
304 # --------------------------------------------------------------------------
305 package OpenILS::Application::Circ::Circulator;
306 use strict; use warnings;
307 use vars q/$AUTOLOAD/;
309 use OpenILS::Utils::Fieldmapper;
310 use OpenSRF::Utils::Cache;
311 use Digest::MD5 qw(md5_hex);
312 use DateTime::Format::ISO8601;
313 use OpenILS::Utils::PermitHold;
314 use OpenSRF::Utils qw/:datetime/;
315 use OpenSRF::Utils::SettingsClient;
316 use OpenILS::Application::Circ::Holds;
317 use OpenILS::Application::Circ::Transit;
318 use OpenSRF::Utils::Logger qw(:logger);
319 use OpenILS::Utils::CStoreEditor qw/:funcs/;
320 use OpenILS::Application::Circ::ScriptBuilder;
321 use OpenILS::Const qw/:const/;
322 use OpenILS::Utils::Penalty;
323 use OpenILS::Application::Circ::CircCommon;
326 my $holdcode = "OpenILS::Application::Circ::Holds";
327 my $transcode = "OpenILS::Application::Circ::Transit";
333 # --------------------------------------------------------------------------
334 # Add a pile of automagic getter/setter methods
335 # --------------------------------------------------------------------------
336 my @AUTOLOAD_FIELDS = qw/
380 recurring_fines_level
393 cancelled_hold_transit
399 circ_matrix_matchpoint
401 legacy_script_support
415 my $type = ref($self) or die "$self is not an object";
417 my $name = $AUTOLOAD;
420 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
421 $logger->error("circulator: $type: invalid autoload field: $name");
422 die "$type: invalid autoload field: $name\n"
427 *{"${type}::${name}"} = sub {
430 $s->{$name} = $v if defined $v;
434 return $self->$name($data);
439 my( $class, $auth, %args ) = @_;
440 $class = ref($class) || $class;
441 my $self = bless( {}, $class );
444 $self->editor(new_editor(xact => 1, authtoken => $auth));
446 unless( $self->editor->checkauth ) {
447 $self->bail_on_events($self->editor->event);
451 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
453 $self->$_($args{$_}) for keys %args;
456 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
458 # if this is a renewal, default to desk_renewal
459 $self->desk_renewal(1) unless
460 $self->opac_renewal or $self->phone_renewal;
462 $self->capture('') unless $self->capture;
464 unless(%user_groups) {
465 my $gps = $self->editor->retrieve_all_permission_grp_tree;
466 %user_groups = map { $_->id => $_ } @$gps;
473 # --------------------------------------------------------------------------
474 # True if we should discontinue processing
475 # --------------------------------------------------------------------------
477 my( $self, $bool ) = @_;
478 if( defined $bool ) {
479 $logger->info("circulator: BAILING OUT") if $bool;
480 $self->{bail_out} = $bool;
482 return $self->{bail_out};
487 my( $self, @evts ) = @_;
490 $logger->info("circulator: pushing event ".$e->{textcode});
491 push( @{$self->events}, $e ) unless
492 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
498 my $key = md5_hex( time() . rand() . "$$" );
499 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
500 return $self->permit_key($key);
503 sub check_permit_key {
505 my $key = $self->permit_key;
506 return 0 unless $key;
507 my $k = "oils_permit_key_$key";
508 my $one = $self->cache_handle->get_cache($k);
509 $self->cache_handle->delete_cache($k);
510 return ($one) ? 1 : 0;
515 my $e = $self->editor;
517 # --------------------------------------------------------------------------
518 # Grab the fleshed copy
519 # --------------------------------------------------------------------------
520 unless($self->is_noncat) {
524 flesh_fields => {acp => ['call_number'], acn => ['record']}
527 $copy = $e->retrieve_asset_copy(
528 [$self->copy_id, $flesh ]) or return $e->event;
530 } elsif( $self->copy_barcode ) {
532 $copy = $e->search_asset_copy(
533 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
538 $self->volume($copy->call_number);
539 $self->title($self->volume->record);
540 $self->copy->call_number($self->volume->id);
541 $self->volume->record($self->title->id);
542 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
543 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
544 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
545 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
548 # We can't renew if there is no copy
549 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
550 if $self->is_renewal;
555 # --------------------------------------------------------------------------
557 # --------------------------------------------------------------------------
559 if( $self->patron_id ) {
560 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
562 } elsif( $self->patron_barcode ) {
564 my $card = $e->search_actor_card(
565 {barcode => $self->patron_barcode})->[0] or return $e->event;
567 $patron = $e->search_actor_user(
568 {card => $card->id})->[0] or return $e->event;
571 if( my $copy = $self->copy ) {
572 my $circs = $e->search_action_circulation(
573 {target_copy => $copy->id, checkin_time => undef});
575 if( my $circ = $circs->[0] ) {
576 $patron = $e->retrieve_actor_user($circ->usr)
582 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
583 unless $self->patron($patron) or $self->is_checkin;
586 # --------------------------------------------------------------------------
587 # This builds the script runner environment and fetches most of the
589 # --------------------------------------------------------------------------
590 sub mk_script_runner {
596 qw/copy copy_barcode copy_id patron
597 patron_id patron_barcode volume title editor/;
599 # Translate our objects into the ScriptBuilder args hash
600 $$args{$_} = $self->$_() for @fields;
602 $args->{ignore_user_status} = 1 if $self->is_checkin;
603 $$args{fetch_patron_by_circ_copy} = 1;
604 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
606 if( my $pco = $self->pending_checkouts ) {
607 $logger->info("circulator: we were given a pending checkouts number of $pco");
608 $$args{patronItemsOut} = $pco;
611 # This fetches most of the objects we need
612 $self->script_runner(
613 OpenILS::Application::Circ::ScriptBuilder->build($args));
615 # Now we translate the ScriptBuilder objects back into self
616 $self->$_($$args{$_}) for @fields;
618 my @evts = @{$args->{_events}} if $args->{_events};
620 $logger->debug("circulator: script builder returned events: @evts") if @evts;
624 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
625 if(!$self->is_noncat and
627 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
631 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
632 return $self->bail_on_events(@e);
637 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
638 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
639 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
640 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
644 # We can't renew if there is no copy
645 return $self->bail_on_events(@evts) if
646 $self->is_renewal and !$self->copy;
648 # Set some circ-specific flags in the script environment
649 my $evt = "environment";
650 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
652 if( $self->is_noncat ) {
653 $self->script_runner->insert("$evt.isNonCat", 1);
654 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
657 if( $self->is_precat ) {
658 $self->script_runner->insert("environment.isPrecat", 1, 1);
661 $self->script_runner->add_path( $_ ) for @$script_libs;
666 # --------------------------------------------------------------------------
667 # Does the circ permit work
668 # --------------------------------------------------------------------------
672 $self->log_me("do_permit()");
674 unless( $self->editor->requestor->id == $self->patron->id ) {
675 return $self->bail_on_events($self->editor->event)
676 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
679 $self->check_captured_holds();
680 $self->do_copy_checks();
681 return if $self->bail_out;
682 $self->run_patron_permit_scripts();
683 $self->run_copy_permit_scripts()
684 unless $self->is_precat or $self->is_noncat;
685 $self->check_item_deposit_events();
686 $self->override_events();
687 return if $self->bail_out;
689 if($self->is_precat and not $self->request_precat) {
692 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
693 return $self->bail_out(1) unless $self->is_renewal;
697 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
700 sub check_item_deposit_events {
702 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
703 if $self->is_deposit and not $self->is_deposit_exempt;
704 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
705 if $self->is_rental and not $self->is_rental_exempt;
708 # returns true if the user is not required to pay deposits
709 sub is_deposit_exempt {
711 my $pid = (ref $self->patron->profile) ?
712 $self->patron->profile->id : $self->patron->profile;
713 my $groups = $U->ou_ancestor_setting_value(
714 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
715 for my $grp (@$groups) {
716 return 1 if $self->is_group_descendant($grp, $pid);
721 # returns true if the user is not required to pay rental fees
722 sub is_rental_exempt {
724 my $pid = (ref $self->patron->profile) ?
725 $self->patron->profile->id : $self->patron->profile;
726 my $groups = $U->ou_ancestor_setting_value(
727 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
728 for my $grp (@$groups) {
729 return 1 if $self->is_group_descendant($grp, $pid);
734 sub is_group_descendant {
735 my($self, $p_id, $c_id) = @_;
736 return 0 unless defined $p_id and defined $c_id;
737 return 1 if $c_id == $p_id;
738 while(my $grp = $user_groups{$c_id}) {
739 $c_id = $grp->parent;
740 return 0 unless defined $c_id;
741 return 1 if $c_id == $p_id;
746 sub check_captured_holds {
748 my $copy = $self->copy;
749 my $patron = $self->patron;
751 return undef unless $copy;
753 my $s = $U->copy_status($copy->status)->id;
754 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
755 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
757 # Item is on the holds shelf, make sure it's going to the right person
758 my $holds = $self->editor->search_action_hold_request(
761 current_copy => $copy->id ,
762 capture_time => { '!=' => undef },
763 cancel_time => undef,
764 fulfillment_time => undef
770 if( $holds and $$holds[0] ) {
771 return undef if $$holds[0]->usr == $patron->id;
774 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
776 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
782 my $copy = $self->copy;
785 my $stat = $U->copy_status($copy->status)->id;
787 # We cannot check out a copy if it is in-transit
788 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
789 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
792 $self->handle_claims_returned();
793 return if $self->bail_out;
795 # no claims returned circ was found, check if there is any open circ
796 unless( $self->is_renewal ) {
798 my $circs = $self->editor->search_action_circulation(
799 { target_copy => $copy->id, checkin_time => undef }
802 if(my $old_circ = $circs->[0]) { # an open circ was found
804 my $payload; # event payload
806 if($old_circ->usr == $self->patron->id) {
808 $payload = {old_circ => $old_circ};
810 # If there is an open circulation on the checkout item and an auto-renew
811 # interval is defined, inform the caller that they should go
812 # ahead and renew the item instead of warning about open circulations.
814 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
815 $self->editor->requestor->ws_ou,
816 'circ.checkout_auto_renew_age',
820 if($auto_renew_intvl) {
821 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
822 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($old_circ->xact_start) );
824 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
825 $payload->{auto_renew} = 1;
830 return $self->bail_on_events(
831 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
837 my $LEGACY_CIRC_EVENT_MAP = {
838 'actor.usr.barred' => 'PATRON_BARRED',
839 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
840 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
841 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
842 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
843 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
844 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
845 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
846 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
850 # ---------------------------------------------------------------------
851 # This pushes any patron-related events into the list but does not
852 # set bail_out for any events
853 # ---------------------------------------------------------------------
854 sub run_patron_permit_scripts {
856 my $runner = $self->script_runner;
857 my $patronid = $self->patron->id;
861 if(!$self->legacy_script_support) {
863 my $results = $self->run_indb_circ_test;
864 unless($self->circ_test_success) {
865 push(@allevents, OpenILS::Event->new(
866 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
872 # ---------------------------------------------------------------------
873 # # Now run the patron permit script
874 # ---------------------------------------------------------------------
875 $runner->load($self->circ_permit_patron);
876 my $result = $runner->run or
877 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
879 my $patron_events = $result->{events};
881 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
882 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
883 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
884 $penalties = $penalties->{fatal_penalties};
886 for my $pen (@$penalties) {
887 my $event = OpenILS::Event->new($pen->name);
888 $event->{desc} = $pen->label;
889 push(@allevents, $event);
892 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
895 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
897 $self->push_events(@allevents);
900 sub run_indb_circ_test {
902 return $self->matrix_test_result if $self->matrix_test_result;
904 my $dbfunc = ($self->is_renewal) ?
905 'action.item_user_renew_test' : 'action.item_user_circ_test';
907 my $results = $self->editor->json_query(
910 $self->editor->requestor->ws_ou,
911 ($self->is_precat or $self->is_noncat) ? undef : $self->copy->id,
917 $self->circ_test_success($U->is_true($results->[0]->{success}));
919 if(my $mp = $results->[0]->{matchpoint}) {
920 $self->circ_matrix_matchpoint(
921 $self->editor->retrieve_config_circ_matrix_matchpoint([
924 flesh_fields => {ccmm =>
925 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
931 return $self->matrix_test_result($results);
934 # ---------------------------------------------------------------------
935 # given a use and copy, this will calculate the circulation policy
936 # parameters. Only works with in-db circ.
937 # ---------------------------------------------------------------------
941 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
943 $self->run_indb_circ_test;
946 circ_test_success => $self->circ_test_success,
947 failure_events => [],
951 unless($self->circ_test_success) {
952 push(@{$results->{failure_codes}},
953 $_->{fail_part}) for @{$self->matrix_test_result};
954 push(@{$results->{failure_events}},
955 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part})
956 for @{$self->matrix_test_result};
959 if($self->circ_matrix_matchpoint) {
960 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
961 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
962 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
964 my $policy = $self->get_circ_policy(
965 $duration_rule, $recurring_fine_rule, $max_fine_rule);
967 $$results{$_} = $$policy{$_} for keys %$policy;
973 # ---------------------------------------------------------------------
974 # Loads the circ policy info for duration, recurring fine, and max
975 # fine based on the current copy
976 # ---------------------------------------------------------------------
977 sub get_circ_policy {
978 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
981 duration_rule => $duration_rule->name,
982 recurring_fine_rule => $recurring_fine_rule->name,
983 max_fine_rule => $max_fine_rule->name,
984 max_fine => $self->get_max_fine_amount($max_fine_rule),
985 fine_interval => $recurring_fine_rule->recurance_interval,
986 renewal_remaining => $duration_rule->max_renewals
989 $policy->{duration} = $duration_rule->shrt
990 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
991 $policy->{duration} = $duration_rule->normal
992 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
993 $policy->{duration} = $duration_rule->extended
994 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
996 $policy->{recurring_fine} = $recurring_fine_rule->low
997 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
998 $policy->{recurring_fine} = $recurring_fine_rule->normal
999 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1000 $policy->{recurring_fine} = $recurring_fine_rule->high
1001 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1006 sub get_max_fine_amount {
1008 my $max_fine_rule = shift;
1009 my $max_amount = $max_fine_rule->amount;
1011 # if is_percent is true then the max->amount is
1012 # use as a percentage of the copy price
1013 if ($U->is_true($max_fine_rule->is_percent)) {
1014 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1015 $max_amount = $price * $max_fine_rule->amount / 100;
1023 sub run_copy_permit_scripts {
1025 my $copy = $self->copy || return;
1026 my $runner = $self->script_runner;
1030 if(!$self->legacy_script_support) {
1031 my $results = $self->run_indb_circ_test;
1032 unless($self->circ_test_success) {
1033 push(@allevents, OpenILS::Event->new(
1034 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
1039 # ---------------------------------------------------------------------
1040 # Capture all of the copy permit events
1041 # ---------------------------------------------------------------------
1042 $runner->load($self->circ_permit_copy);
1043 my $result = $runner->run or
1044 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1045 my $copy_events = $result->{events};
1047 # ---------------------------------------------------------------------
1048 # Now collect all of the events together
1049 # ---------------------------------------------------------------------
1050 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1053 # See if this copy has an alert message
1054 my $ae = $self->check_copy_alert();
1055 push( @allevents, $ae ) if $ae;
1057 # uniquify the events
1058 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1059 @allevents = values %hash;
1062 $_->{payload} = $copy if
1063 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1066 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1068 $self->push_events(@allevents);
1072 sub check_copy_alert {
1074 return undef if $self->is_renewal;
1075 return OpenILS::Event->new(
1076 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1077 if $self->copy and $self->copy->alert_message;
1083 # --------------------------------------------------------------------------
1084 # If the call is overriding and has permissions to override every collected
1085 # event, the are cleared. Any event that the caller does not have
1086 # permission to override, will be left in the event list and bail_out will
1088 # XXX We need code in here to cancel any holds/transits on copies
1089 # that are being force-checked out
1090 # --------------------------------------------------------------------------
1091 sub override_events {
1093 my @events = @{$self->events};
1094 return unless @events;
1096 if(!$self->override) {
1097 return $self->bail_out(1)
1098 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1103 for my $e (@events) {
1104 my $tc = $e->{textcode};
1105 next if $tc eq 'SUCCESS';
1106 my $ov = "$tc.override";
1107 $logger->info("circulator: attempting to override event: $ov");
1109 return $self->bail_on_events($self->editor->event)
1110 unless( $self->editor->allowed($ov) );
1115 # --------------------------------------------------------------------------
1116 # If there is an open claimsreturn circ on the requested copy, close the
1117 # circ if overriding, otherwise bail out
1118 # --------------------------------------------------------------------------
1119 sub handle_claims_returned {
1121 my $copy = $self->copy;
1123 my $CR = $self->editor->search_action_circulation(
1125 target_copy => $copy->id,
1126 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1127 checkin_time => undef,
1131 return unless ($CR = $CR->[0]);
1135 # - If the caller has set the override flag, we will check the item in
1136 if($self->override) {
1138 $CR->checkin_time('now');
1139 $CR->checkin_scan_time('now');
1140 $CR->checkin_lib($self->editor->requestor->ws_ou);
1141 $CR->checkin_workstation($self->editor->requestor->wsid);
1142 $CR->checkin_staff($self->editor->requestor->id);
1144 $evt = $self->editor->event
1145 unless $self->editor->update_action_circulation($CR);
1148 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1151 $self->bail_on_events($evt) if $evt;
1156 # --------------------------------------------------------------------------
1157 # This performs the checkout
1158 # --------------------------------------------------------------------------
1162 $self->log_me("do_checkout()");
1164 # make sure perms are good if this isn't a renewal
1165 unless( $self->is_renewal ) {
1166 return $self->bail_on_events($self->editor->event)
1167 unless( $self->editor->allowed('COPY_CHECKOUT') );
1170 # verify the permit key
1171 unless( $self->check_permit_key ) {
1172 if( $self->permit_override ) {
1173 return $self->bail_on_events($self->editor->event)
1174 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1176 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1180 # if this is a non-cataloged circ, build the circ and finish
1181 if( $self->is_noncat ) {
1182 $self->checkout_noncat;
1184 OpenILS::Event->new('SUCCESS',
1185 payload => { noncat_circ => $self->circ }));
1189 if( $self->is_precat ) {
1190 $self->make_precat_copy;
1191 return if $self->bail_out;
1193 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1194 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1197 $self->do_copy_checks;
1198 return if $self->bail_out;
1200 $self->run_checkout_scripts();
1201 return if $self->bail_out;
1203 $self->build_checkout_circ_object();
1204 return if $self->bail_out;
1206 $self->apply_modified_due_date();
1207 return if $self->bail_out;
1209 return $self->bail_on_events($self->editor->event)
1210 unless $self->editor->create_action_circulation($self->circ);
1212 # refresh the circ to force local time zone for now
1213 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1215 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1217 return if $self->bail_out;
1219 $self->apply_deposit_fee();
1220 return if $self->bail_out;
1222 $self->handle_checkout_holds();
1223 return if $self->bail_out;
1225 # ------------------------------------------------------------------------------
1226 # Update the patron penalty info in the DB. Run it for permit-overrides
1227 # since the penalties are not updated during the permit phase
1228 # ------------------------------------------------------------------------------
1229 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1231 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1233 OpenILS::Event->new('SUCCESS',
1235 copy => $U->unflesh_copy($self->copy),
1236 circ => $self->circ,
1238 holds_fulfilled => $self->fulfilled_holds,
1239 deposit_billing => $self->deposit_billing,
1240 rental_billing => $self->rental_billing
1246 sub apply_deposit_fee {
1248 my $copy = $self->copy;
1250 ($self->is_deposit and not $self->is_deposit_exempt) or
1251 ($self->is_rental and not $self->is_rental_exempt);
1253 my $bill = Fieldmapper::money::billing->new;
1254 my $amount = $copy->deposit_amount;
1258 if($self->is_deposit) {
1259 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1261 $self->deposit_billing($bill);
1263 $billing_type = OILS_BILLING_TYPE_RENTAL;
1265 $self->rental_billing($bill);
1268 $bill->xact($self->circ->id);
1269 $bill->amount($amount);
1270 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1271 $bill->billing_type($billing_type);
1272 $bill->btype($btype);
1273 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1275 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1280 my $copy = $self->copy;
1282 my $stat = $copy->status if ref $copy->status;
1283 my $loc = $copy->location if ref $copy->location;
1284 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1286 $copy->status($stat->id) if $stat;
1287 $copy->location($loc->id) if $loc;
1288 $copy->circ_lib($circ_lib->id) if $circ_lib;
1289 $copy->editor($self->editor->requestor->id);
1290 $copy->edit_date('now');
1291 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1293 return $self->bail_on_events($self->editor->event)
1294 unless $self->editor->update_asset_copy($self->copy);
1296 $copy->status($U->copy_status($copy->status));
1297 $copy->location($loc) if $loc;
1298 $copy->circ_lib($circ_lib) if $circ_lib;
1302 sub bail_on_events {
1303 my( $self, @evts ) = @_;
1304 $self->push_events(@evts);
1309 # ------------------------------------------------------------------------------
1310 # When an item is checked out, see if we can fulfill a hold for this patron
1311 # ------------------------------------------------------------------------------
1312 sub handle_checkout_holds {
1314 my $copy = $self->copy;
1315 my $patron = $self->patron;
1317 my $e = $self->editor;
1318 $self->fulfilled_holds([]);
1320 # pre/non-cats can't fulfill a hold
1321 return if $self->is_precat or $self->is_noncat;
1323 my $hold = $e->search_action_hold_request({
1324 current_copy => $copy->id ,
1325 cancel_time => undef,
1326 fulfillment_time => undef,
1328 {expire_time => undef},
1329 {expire_time => {'>' => 'now'}}
1333 if($hold and $hold->usr != $patron->id) {
1334 # reset the hold since the copy is now checked out
1336 $logger->info("circulator: un-targeting hold ".$hold->id.
1337 " because copy ".$copy->id." is getting checked out");
1339 $hold->clear_prev_check_time;
1340 $hold->clear_current_copy;
1341 $hold->clear_capture_time;
1343 return $self->bail_on_event($e->event)
1344 unless $e->update_action_hold_request($hold);
1350 $hold = $self->find_related_user_hold($copy, $patron) or return;
1351 $logger->info("circulator: found related hold to fulfill in checkout");
1354 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1356 # if the hold was never officially captured, capture it.
1357 $hold->current_copy($copy->id);
1358 $hold->capture_time('now') unless $hold->capture_time;
1359 $hold->fulfillment_time('now');
1360 $hold->fulfillment_staff($e->requestor->id);
1361 $hold->fulfillment_lib($e->requestor->ws_ou);
1363 return $self->bail_on_events($e->event)
1364 unless $e->update_action_hold_request($hold);
1366 $holdcode->delete_hold_copy_maps($e, $hold->id);
1367 return $self->fulfilled_holds([$hold->id]);
1371 # ------------------------------------------------------------------------------
1372 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1373 # the patron directly targets the checked out item, see if there is another hold
1374 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1375 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1376 # ------------------------------------------------------------------------------
1377 sub find_related_user_hold {
1378 my($self, $copy, $patron) = @_;
1379 my $e = $self->editor;
1381 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1383 return undef unless $U->ou_ancestor_setting_value(
1384 $e->requestor->ws_ou, 'circ.checkout_fills_related_hold', $e);
1386 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1388 select => {ahr => ['id']},
1393 fkey => 'current_copy',
1394 type => 'left' # there may be no current_copy
1401 fulfillment_time => undef,
1402 cancel_time => undef,
1404 {expire_time => undef},
1405 {expire_time => {'>' => 'now'}}
1412 target => $self->volume->id
1418 target => $self->title->id
1424 {id => undef}, # left-join copy may be nonexistent
1425 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1429 order_by => {ahr => {request_time => {direction => 'asc'}}},
1433 my $hold_info = $e->json_query($args)->[0];
1434 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1439 sub run_checkout_scripts {
1443 my $runner = $self->script_runner;
1452 if(!$self->legacy_script_support) {
1453 $self->run_indb_circ_test();
1454 $duration = $self->circ_matrix_matchpoint->duration_rule;
1455 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1456 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1460 $runner->load($self->circ_duration);
1462 my $result = $runner->run or
1463 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1465 $duration_name = $result->{durationRule};
1466 $recurring_name = $result->{recurringFinesRule};
1467 $max_fine_name = $result->{maxFine};
1470 $duration_name = $duration->name if $duration;
1471 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1474 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1475 return $self->bail_on_events($evt) if $evt;
1477 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1478 return $self->bail_on_events($evt) if $evt;
1480 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1481 return $self->bail_on_events($evt) if $evt;
1486 # The item circulates with an unlimited duration
1492 $self->duration_rule($duration);
1493 $self->recurring_fines_rule($recurring);
1494 $self->max_fine_rule($max_fine);
1498 sub build_checkout_circ_object {
1501 my $circ = Fieldmapper::action::circulation->new;
1502 my $duration = $self->duration_rule;
1503 my $max = $self->max_fine_rule;
1504 my $recurring = $self->recurring_fines_rule;
1505 my $copy = $self->copy;
1506 my $patron = $self->patron;
1510 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1512 my $dname = $duration->name;
1513 my $mname = $max->name;
1514 my $rname = $recurring->name;
1516 $logger->debug("circulator: building circulation ".
1517 "with duration=$dname, maxfine=$mname, recurring=$rname");
1519 $circ->duration($policy->{duration});
1520 $circ->recuring_fine($policy->{recurring_fine});
1521 $circ->duration_rule($duration->name);
1522 $circ->recuring_fine_rule($recurring->name);
1523 $circ->max_fine_rule($max->name);
1524 $circ->max_fine($policy->{max_fine});
1525 $circ->fine_interval($recurring->recurance_interval);
1526 $circ->renewal_remaining($duration->max_renewals);
1530 $logger->info("circulator: copy found with an unlimited circ duration");
1531 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1532 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1533 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1534 $circ->renewal_remaining(0);
1537 $circ->target_copy( $copy->id );
1538 $circ->usr( $patron->id );
1539 $circ->circ_lib( $self->circ_lib );
1540 $circ->workstation($self->editor->requestor->wsid)
1541 if defined $self->editor->requestor->wsid;
1543 # renewals maintain a link to the parent circulation
1544 $circ->parent_circ($self->parent_circ);
1546 if( $self->is_renewal ) {
1547 $circ->opac_renewal('t') if $self->opac_renewal;
1548 $circ->phone_renewal('t') if $self->phone_renewal;
1549 $circ->desk_renewal('t') if $self->desk_renewal;
1550 $circ->renewal_remaining($self->renewal_remaining);
1551 $circ->circ_staff($self->editor->requestor->id);
1555 # if the user provided an overiding checkout time,
1556 # (e.g. the checkout really happened several hours ago), then
1557 # we apply that here. Does this need a perm??
1558 $circ->xact_start(clense_ISO8601($self->checkout_time))
1559 if $self->checkout_time;
1561 # if a patron is renewing, 'requestor' will be the patron
1562 $circ->circ_staff($self->editor->requestor->id);
1563 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1569 sub apply_modified_due_date {
1571 my $circ = $self->circ;
1572 my $copy = $self->copy;
1574 if( $self->due_date ) {
1576 return $self->bail_on_events($self->editor->event)
1577 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1579 $circ->due_date(clense_ISO8601($self->due_date));
1583 # if the due_date lands on a day when the location is closed
1584 return unless $copy and $circ->due_date;
1586 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1588 # due-date overlap should be determined by the location the item
1589 # is checked out from, not the owning or circ lib of the item
1590 my $org = $self->editor->requestor->ws_ou;
1592 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1593 " with an item due date of ".$circ->due_date );
1595 my $dateinfo = $U->storagereq(
1596 'open-ils.storage.actor.org_unit.closed_date.overlap',
1597 $org, $circ->due_date );
1600 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1601 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1603 # XXX make the behavior more dynamic
1604 # for now, we just push the due date to after the close date
1605 $circ->due_date($dateinfo->{end});
1612 sub create_due_date {
1613 my( $self, $duration ) = @_;
1615 # if there is a raw time component (e.g. from postgres),
1616 # turn it into an interval that interval_to_seconds can parse
1617 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1619 # for now, use the server timezone. TODO: use workstation org timezone
1620 my $due_date = DateTime->now(time_zone => 'local');
1622 # add the circ duration
1623 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
1625 # return ISO8601 time with timezone
1626 return $due_date->strftime('%FT%T%z');
1631 sub make_precat_copy {
1633 my $copy = $self->copy;
1636 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1638 $copy->editor($self->editor->requestor->id);
1639 $copy->edit_date('now');
1640 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
1641 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
1642 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
1643 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
1644 $self->update_copy();
1648 $logger->info("circulator: Creating a new precataloged ".
1649 "copy in checkout with barcode " . $self->copy_barcode);
1651 $copy = Fieldmapper::asset::copy->new;
1652 $copy->circ_lib($self->circ_lib);
1653 $copy->creator($self->editor->requestor->id);
1654 $copy->editor($self->editor->requestor->id);
1655 $copy->barcode($self->copy_barcode);
1656 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1657 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1658 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1660 $copy->dummy_title($self->dummy_title || "");
1661 $copy->dummy_author($self->dummy_author || "");
1662 $copy->dummy_isbn($self->dummy_isbn || "");
1663 $copy->circ_modifier($self->circ_modifier);
1666 # See if we need to override the circ_lib for the copy with a configured circ_lib
1667 # Setting is shortname of the org unit
1668 my $precat_circ_lib = $U->ou_ancestor_setting_value(
1669 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
1671 if($precat_circ_lib) {
1672 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
1675 $self->bail_on_events($self->editor->event);
1679 $copy->circ_lib($org->id);
1683 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1685 $self->push_events($self->editor->event);
1689 # this is a little bit of a hack, but we need to
1690 # get the copy into the script runner
1691 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1695 sub checkout_noncat {
1701 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1702 my $count = $self->noncat_count || 1;
1703 my $cotime = clense_ISO8601($self->checkout_time) || "";
1705 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
1709 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1710 $self->editor->requestor->id,
1718 $self->push_events($evt);
1729 $self->log_me("do_checkin()");
1731 return $self->bail_on_events(
1732 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1735 if( $self->checkin_check_holds_shelf() ) {
1736 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1737 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1738 $self->checkin_flesh_events;
1742 unless( $self->is_renewal ) {
1743 return $self->bail_on_events($self->editor->event)
1744 unless $self->editor->allowed('COPY_CHECKIN');
1747 $self->push_events($self->check_copy_alert());
1748 $self->push_events($self->check_checkin_copy_status());
1750 # the renew code will have already found our circulation object
1751 unless( $self->is_renewal and $self->circ ) {
1752 my $circs = $self->editor->search_action_circulation(
1753 { target_copy => $self->copy->id, checkin_time => undef });
1754 $self->circ($$circs[0]);
1756 # for now, just warn if there are multiple open circs on a copy
1757 $logger->warn("circulator: we have ".scalar(@$circs).
1758 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1761 # run the fine generator against this circ, if this circ is there
1762 $self->generate_fines if ($self->circ);
1764 # if the circ is marked as 'claims returned', add the event to the list
1765 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1766 if ($self->circ and $self->circ->stop_fines
1767 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1769 $self->check_circ_deposit();
1771 # handle the overridable events
1772 $self->override_events unless $self->is_renewal;
1773 return if $self->bail_out;
1777 $self->editor->search_action_transit_copy(
1778 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1782 $self->checkin_handle_circ;
1783 return if $self->bail_out;
1784 $self->checkin_changed(1);
1786 } elsif( $self->transit ) {
1787 my $hold_transit = $self->process_received_transit;
1788 $self->checkin_changed(1);
1790 if( $self->bail_out ) {
1791 $self->checkin_flesh_events;
1795 if( my $e = $self->check_checkin_copy_status() ) {
1796 # If the original copy status is special, alert the caller
1797 my $ev = $self->events;
1798 $self->events([$e]);
1799 $self->override_events;
1800 return if $self->bail_out;
1804 if( $hold_transit or
1805 $U->copy_status($self->copy->status)->id
1806 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1809 if( $hold_transit ) {
1810 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1812 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1817 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1819 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1820 $self->reshelve_copy(1);
1821 $self->cancelled_hold_transit(1);
1822 $self->notify_hold(0); # don't notify for cancelled holds
1823 return if $self->bail_out;
1827 # hold transited to correct location
1828 $self->checkin_flesh_events;
1833 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1835 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1836 " that is in-transit, but there is no transit.. repairing");
1837 $self->reshelve_copy(1);
1838 return if $self->bail_out;
1841 if( $self->is_renewal ) {
1842 $self->push_events(OpenILS::Event->new('SUCCESS'));
1846 # ------------------------------------------------------------------------------
1847 # Circulations and transits are now closed where necessary. Now go on to see if
1848 # this copy can fulfill a hold or needs to be routed to a different location
1849 # ------------------------------------------------------------------------------
1851 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1853 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1854 return if $self->bail_out;
1856 unless($needed_for_hold) {
1857 my $circ_lib = (ref $self->copy->circ_lib) ?
1858 $self->copy->circ_lib->id : $self->copy->circ_lib;
1860 if( $self->remote_hold ) {
1861 $circ_lib = $self->remote_hold->pickup_lib;
1862 $logger->warn("circulator: Copy ".$self->copy->barcode.
1863 " is on a remote hold's shelf, sending to $circ_lib");
1866 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1868 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1870 $self->checkin_handle_precat();
1871 return if $self->bail_out;
1875 my $bc = $self->copy->barcode;
1876 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1877 $self->checkin_build_copy_transit($circ_lib);
1878 return if $self->bail_out;
1879 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1884 $self->reshelve_copy;
1885 return if $self->bail_out;
1887 unless($self->checkin_changed) {
1889 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1890 my $stat = $U->copy_status($self->copy->status)->id;
1892 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1893 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1894 $self->bail_out(1); # no need to commit anything
1898 $self->push_events(OpenILS::Event->new('SUCCESS'))
1899 unless @{$self->events};
1902 OpenILS::Utils::Penalty->calculate_penalties(
1903 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1905 $self->checkin_flesh_events;
1909 # if a deposit was payed for this item, push the event
1910 sub check_circ_deposit {
1912 return unless $self->circ;
1913 my $deposit = $self->editor->search_money_billing(
1915 xact => $self->circ->id,
1917 }, {idlist => 1})->[0];
1919 $self->push_events(OpenILS::Event->new(
1920 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1925 my $force = $self->force || shift;
1926 my $copy = $self->copy;
1928 my $stat = $U->copy_status($copy->status)->id;
1931 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1932 $stat != OILS_COPY_STATUS_CATALOGING and
1933 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1934 $stat != OILS_COPY_STATUS_RESHELVING )) {
1936 $copy->status( OILS_COPY_STATUS_RESHELVING );
1938 $self->checkin_changed(1);
1943 # Returns true if the item is at the current location
1944 # because it was transited there for a hold and the
1945 # hold has not been fulfilled
1946 sub checkin_check_holds_shelf {
1948 return 0 unless $self->copy;
1951 $U->copy_status($self->copy->status)->id ==
1952 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1954 # find the hold that put us on the holds shelf
1955 my $holds = $self->editor->search_action_hold_request(
1957 current_copy => $self->copy->id,
1958 capture_time => { '!=' => undef },
1959 fulfillment_time => undef,
1960 cancel_time => undef,
1965 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1966 $self->reshelve_copy(1);
1970 my $hold = $$holds[0];
1972 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1973 $hold->id. "] for copy ".$self->copy->barcode);
1975 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1976 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1980 $logger->info("circulator: hold is not for here..");
1981 $self->remote_hold($hold);
1986 sub checkin_handle_precat {
1988 my $copy = $self->copy;
1990 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1991 $copy->status(OILS_COPY_STATUS_CATALOGING);
1992 $self->update_copy();
1993 $self->checkin_changed(1);
1994 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1999 sub checkin_build_copy_transit {
2002 my $copy = $self->copy;
2003 my $transit = Fieldmapper::action::transit_copy->new;
2005 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2006 $logger->info("circulator: transiting copy to $dest");
2008 $transit->source($self->editor->requestor->ws_ou);
2009 $transit->dest($dest);
2010 $transit->target_copy($copy->id);
2011 $transit->source_send_time('now');
2012 $transit->copy_status( $U->copy_status($copy->status)->id );
2014 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2016 return $self->bail_on_events($self->editor->event)
2017 unless $self->editor->create_action_transit_copy($transit);
2019 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2021 $self->checkin_changed(1);
2025 # returns true if the item was used (or may potentially be used
2026 # in subsequent calls) to capture a hold.
2027 sub attempt_checkin_hold_capture {
2029 my $copy = $self->copy;
2031 # we've been explicitly told not to capture any holds
2032 return 0 if $self->capture eq 'nocapture';
2034 # See if this copy can fulfill any holds
2035 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2036 $self->editor, $copy, $self->editor->requestor );
2039 $logger->debug("circulator: no potential permitted".
2040 "holds found for copy ".$copy->barcode);
2044 if($self->capture ne 'capture') {
2045 # see if this item is in a hold-capture-delay location
2046 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2047 if($U->is_true($location->hold_verify)) {
2048 $self->bail_on_events(
2049 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2054 $self->retarget($retarget);
2056 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2058 $hold->current_copy($copy->id);
2059 $hold->capture_time('now');
2060 $hold->shelf_time('now')
2061 if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2063 # prevent DB errors caused by fetching
2064 # holds from storage, and updating through cstore
2065 $hold->clear_fulfillment_time;
2066 $hold->clear_fulfillment_staff;
2067 $hold->clear_fulfillment_lib;
2068 $hold->clear_expire_time;
2069 $hold->clear_cancel_time;
2070 $hold->clear_prev_check_time unless $hold->prev_check_time;
2072 $self->bail_on_events($self->editor->event)
2073 unless $self->editor->update_action_hold_request($hold);
2075 $self->checkin_changed(1);
2077 return 0 if $self->bail_out;
2079 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2081 # This hold was captured in the correct location
2082 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2083 $self->push_events(OpenILS::Event->new('SUCCESS'));
2085 #$self->do_hold_notify($hold->id);
2086 $self->notify_hold($hold->id);
2090 # Hold needs to be picked up elsewhere. Build a hold
2091 # transit and route the item.
2092 $self->checkin_build_hold_transit();
2093 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2094 return 0 if $self->bail_out;
2095 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2098 # make sure we save the copy status
2103 sub do_hold_notify {
2104 my( $self, $holdid ) = @_;
2106 my $e = new_editor(xact => 1);
2107 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2109 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2110 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2112 $logger->info("circulator: running delayed hold notify process");
2114 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2115 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2117 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2118 hold_id => $holdid, requestor => $self->editor->requestor);
2120 $logger->debug("circulator: built hold notifier");
2122 if(!$notifier->event) {
2124 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2126 my $stat = $notifier->send_email_notify;
2127 if( $stat == '1' ) {
2128 $logger->info("circulator: hold notify succeeded for hold $holdid");
2132 $logger->warn("circulator: * hold notify failed for hold $holdid");
2135 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2139 sub retarget_holds {
2141 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2142 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2143 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2144 # no reason to wait for the return value
2148 sub checkin_build_hold_transit {
2151 my $copy = $self->copy;
2152 my $hold = $self->hold;
2153 my $trans = Fieldmapper::action::hold_transit_copy->new;
2155 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2157 $trans->hold($hold->id);
2158 $trans->source($self->editor->requestor->ws_ou);
2159 $trans->dest($hold->pickup_lib);
2160 $trans->source_send_time("now");
2161 $trans->target_copy($copy->id);
2163 # when the copy gets to its destination, it will recover
2164 # this status - put it onto the holds shelf
2165 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2167 return $self->bail_on_events($self->editor->event)
2168 unless $self->editor->create_action_hold_transit_copy($trans);
2173 sub process_received_transit {
2175 my $copy = $self->copy;
2176 my $copyid = $self->copy->id;
2178 my $status_name = $U->copy_status($copy->status)->name;
2179 $logger->debug("circulator: attempting transit receive on ".
2180 "copy $copyid. Copy status is $status_name");
2182 my $transit = $self->transit;
2184 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2185 # - this item is in-transit to a different location
2187 my $tid = $transit->id;
2188 my $loc = $self->editor->requestor->ws_ou;
2189 my $dest = $transit->dest;
2191 $logger->info("circulator: Fowarding transit on copy which is destined ".
2192 "for a different location. transit=$tid, copy=$copyid, current ".
2193 "location=$loc, destination location=$dest");
2195 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2197 # grab the associated hold object if available
2198 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2199 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2201 return $self->bail_on_events($evt);
2204 # The transit is received, set the receive time
2205 $transit->dest_recv_time('now');
2206 $self->bail_on_events($self->editor->event)
2207 unless $self->editor->update_action_transit_copy($transit);
2209 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2211 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2212 $copy->status( $transit->copy_status );
2213 $self->update_copy();
2214 return if $self->bail_out;
2218 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2220 # hold has arrived at destination, set shelf time
2221 $hold->shelf_time('now');
2222 $self->bail_on_events($self->editor->event)
2223 unless $self->editor->update_action_hold_request($hold);
2224 return if $self->bail_out;
2226 $self->notify_hold($hold_transit->hold);
2231 OpenILS::Event->new(
2234 payload => { transit => $transit, holdtransit => $hold_transit } ));
2236 return $hold_transit;
2240 sub generate_fines {
2245 my $st = OpenSRF::AppSession->connect('open-ils.storage');
2248 'open-ils.storage.action.circulation.overdue.generate_fines',
2255 # refresh the circ in case the fine generator set the stop_fines field
2256 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
2261 sub checkin_handle_circ {
2263 my $circ = $self->circ;
2264 my $copy = $self->copy;
2268 # backdate the circ if necessary
2269 if($self->backdate) {
2270 $self->checkin_handle_backdate;
2271 return if $self->bail_out;
2274 if($self->void_overdues) {
2275 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2276 $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2277 return $self->bail_on_events($evt) if $evt;
2280 if(!$circ->stop_fines) {
2281 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2282 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2283 $circ->stop_fines_time('now') unless $self->backdate;
2284 $circ->stop_fines_time($self->backdate) if $self->backdate;
2287 # see if there are any fines owed on this circ. if not, close it
2288 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2289 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2291 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2293 # Set the checkin vars since we have the item
2294 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2296 # capture the true scan time for back-dated checkins
2297 $circ->checkin_scan_time('now');
2299 $circ->checkin_staff($self->editor->requestor->id);
2300 $circ->checkin_lib($self->editor->requestor->ws_ou);
2301 $circ->checkin_workstation($self->editor->requestor->wsid);
2303 my $circ_lib = (ref $self->copy->circ_lib) ?
2304 $self->copy->circ_lib->id : $self->copy->circ_lib;
2305 my $stat = $U->copy_status($self->copy->status)->id;
2307 # immediately available keeps items lost or missing items from going home before being handled
2308 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2309 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2312 if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2314 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2315 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2317 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2321 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2323 $self->checkin_handle_lost($circ_lib);
2327 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2331 return $self->bail_on_events($self->editor->event)
2332 unless $self->editor->update_action_circulation($circ);
2334 # make sure the circ isn't closed if we just voided some fines
2335 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2336 return $self->bail_on_events($evt) if $evt;
2342 # ------------------------------------------------------------------
2343 # See if we need to void billings for lost checkin
2344 # ------------------------------------------------------------------
2345 sub checkin_handle_lost {
2347 my $circ_lib = shift;
2348 my $circ = $self->circ;
2350 my $max_return = $U->ou_ancestor_setting_value(
2351 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2356 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2357 $tm[5] -= 1 if $tm[5] > 0;
2358 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2360 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2361 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2363 $max_return = 0 if $today < $last_chance;
2366 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2368 my $void_lost = $U->ou_ancestor_setting_value(
2369 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2370 my $void_lost_fee = $U->ou_ancestor_setting_value(
2371 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2372 my $restore_od = $U->ou_ancestor_setting_value(
2373 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2375 $self->checkin_handle_lost_now_found(3) if $void_lost;
2376 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2377 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2380 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2385 sub checkin_handle_backdate {
2388 my $bd = $self->backdate;
2390 # ------------------------------------------------------------------
2391 # clean up the backdate for date comparison
2392 # we want any bills created on or after the backdate
2393 # ------------------------------------------------------------------
2394 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2395 #$bd = "${bd}T23:59:59";
2397 my $bills = $self->editor->search_money_billing(
2399 billing_ts => { '>=' => $bd },
2400 xact => $self->circ->id,
2405 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2407 for my $bill (@$bills) {
2408 unless( $U->is_true($bill->voided) ) {
2409 $logger->info("backdate voiding bill ".$bill->id);
2411 $bill->void_time('now');
2412 $bill->voider($self->editor->requestor->id);
2413 my $n = $bill->note || "";
2414 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2416 $self->bail_on_events($self->editor->event)
2417 unless $self->editor->update_money_billing($bill);
2425 sub find_patron_from_copy {
2427 my $circs = $self->editor->search_action_circulation(
2428 { target_copy => $self->copy->id, checkin_time => undef });
2429 my $circ = $circs->[0];
2430 return unless $circ;
2431 my $u = $self->editor->retrieve_actor_user($circ->usr)
2432 or return $self->bail_on_events($self->editor->event);
2436 sub check_checkin_copy_status {
2438 my $copy = $self->copy;
2444 my $status = $U->copy_status($copy->status)->id;
2447 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2448 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2449 $status == OILS_COPY_STATUS_IN_PROCESS ||
2450 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2451 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2452 $status == OILS_COPY_STATUS_CATALOGING ||
2453 $status == OILS_COPY_STATUS_RESHELVING );
2455 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2456 if( $status == OILS_COPY_STATUS_LOST );
2458 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2459 if( $status == OILS_COPY_STATUS_MISSING );
2461 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2466 # --------------------------------------------------------------------------
2467 # On checkin, we need to return as many relevant objects as we can
2468 # --------------------------------------------------------------------------
2469 sub checkin_flesh_events {
2472 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2473 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2474 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2477 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2480 if($self->hold and !$self->hold->cancel_time) {
2481 $hold = $self->hold;
2482 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
2485 for my $evt (@{$self->events}) {
2488 $payload->{copy} = $U->unflesh_copy($self->copy);
2489 $payload->{record} = $record,
2490 $payload->{circ} = $self->circ;
2491 $payload->{transit} = $self->transit;
2492 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2493 $payload->{hold} = $hold;
2494 $evt->{payload} = $payload;
2499 my( $self, $msg ) = @_;
2500 my $bc = ($self->copy) ? $self->copy->barcode :
2503 my $usr = ($self->patron) ? $self->patron->id : "";
2504 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2505 ", recipient=$usr, copy=$bc");
2511 $self->log_me("do_renew()");
2513 # Make sure there is an open circ to renew that is not
2514 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2515 my $usrid = $self->patron->id if $self->patron;
2518 # If we have a patron, match them to the circ
2519 $circ = $self->editor->search_action_circulation(
2520 {target_copy => $self->copy->id, usr => $usrid, stop_fines => undef})->[0];
2522 $circ = $self->editor->search_action_circulation(
2523 {target_copy => $self->copy->id, stop_fines => undef})->[0];
2528 $circ = $self->editor->search_action_circulation(
2529 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2531 $circ = $self->editor->search_action_circulation(
2532 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2536 return $self->bail_on_events($self->editor->event) unless $circ;
2538 # A user is not allowed to renew another user's items without permission
2539 unless( $circ->usr eq $self->editor->requestor->id ) {
2540 return $self->bail_on_events($self->editor->events)
2541 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2544 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2545 if $circ->renewal_remaining < 1;
2547 # -----------------------------------------------------------------
2549 $self->parent_circ($circ->id);
2550 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2553 $self->run_renew_permit;
2556 $self->do_checkin();
2557 return if $self->bail_out;
2559 unless( $self->permit_override ) {
2561 return if $self->bail_out;
2562 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2563 $self->remove_event('ITEM_NOT_CATALOGED');
2566 $self->override_events;
2567 return if $self->bail_out;
2570 $self->do_checkout();
2575 my( $self, $evt ) = @_;
2576 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2577 $logger->debug("circulator: removing event from list: $evt");
2578 my @events = @{$self->events};
2579 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2584 my( $self, $evt ) = @_;
2585 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2586 return grep { $_->{textcode} eq $evt } @{$self->events};
2591 sub run_renew_permit {
2596 if(!$self->legacy_script_support) {
2597 my $results = $self->run_indb_circ_test;
2598 unless($self->circ_test_success) {
2599 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) for @$results;
2604 my $runner = $self->script_runner;
2606 $runner->load($self->circ_permit_renew);
2607 my $result = $runner->run or
2608 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2609 $events = $result->{events};
2610 $self->mk_script_runner;
2613 $logger->activity("circulator: circ_permit_renew for user ".
2614 $self->patron->id." returned events: @$events") if @$events;
2616 $self->push_events(OpenILS::Event->new($_)) for @$events;
2618 $logger->debug("circulator: re-creating script runner to be safe");
2622 sub append_reading_list {
2626 $self->is_checkout and
2631 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2633 # verify history is globally enabled and uses the bucket mechanism
2634 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2635 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2637 unless($htype eq 'bucket') {
2642 # verify the patron wants to retain the hisory
2643 my $setting = $e->search_actor_user_setting(
2644 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2646 unless($setting and $setting->value) {
2651 my $bkt = $e->search_container_copy_bucket(
2652 {owner => $self->patron->id, btype => 'circ_history'})->[0];
2657 # find the next item position
2658 my $last_item = $e->search_container_copy_bucket_item(
2659 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2660 $pos = $last_item->pos + 1 if $last_item;
2663 # create the history bucket if necessary
2664 $bkt = Fieldmapper::container::copy_bucket->new;
2665 $bkt->owner($self->patron->id);
2667 $bkt->btype('circ_history');
2669 $e->create_container_copy_bucket($bkt) or return $e->die_event;
2672 my $item = Fieldmapper::container::copy_bucket_item->new;
2674 $item->bucket($bkt->id);
2675 $item->target_copy($self->copy->id);
2678 $e->create_container_copy_bucket_item($item) or return $e->die_event;
2685 sub make_trigger_events {
2687 return unless $self->circ;
2688 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2689 $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2690 $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2691 $ses->request('open-ils.trigger.event.autocreate', 'renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
2698 sub checkin_handle_lost_now_found {
2699 my ($self, $bill_type) = @_;
2701 # ------------------------------------------------------------------
2702 # remove charge from patron's account if lost item is returned
2703 # ------------------------------------------------------------------
2705 my $bills = $self->editor->search_money_billing(
2707 xact => $self->circ->id,
2712 $logger->debug("voiding lost item charge of ".scalar(@$bills));
2713 for my $bill (@$bills) {
2714 if( !$U->is_true($bill->voided) ) {
2715 $logger->info("lost item returned - voiding bill ".$bill->id);
2717 $bill->void_time('now');
2718 $bill->voider($self->editor->requestor->id);
2719 my $note = ($bill->note) ? $bill->note . "\n" : '';
2720 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2722 $self->bail_on_events($self->editor->event)
2723 unless $self->editor->update_money_billing($bill);
2728 sub checkin_handle_lost_now_found_restore_od {
2731 # ------------------------------------------------------------------
2732 # restore those overdue charges voided when item was set to lost
2733 # ------------------------------------------------------------------
2735 my $ods = $self->editor->search_money_billing(
2737 xact => $self->circ->id,
2742 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
2743 for my $bill (@$ods) {
2744 if( $U->is_true($bill->voided) ) {
2745 $logger->info("lost item returned - restoring overdue ".$bill->id);
2747 $bill->clear_void_time;
2748 $bill->voider($self->editor->requestor->id);
2749 my $note = ($bill->note) ? $bill->note . "\n" : '';
2750 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2752 $self->bail_on_events($self->editor->event)
2753 unless $self->editor->update_money_billing($bill);