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
392 cancelled_hold_transit
398 circ_matrix_matchpoint
400 legacy_script_support
414 my $type = ref($self) or die "$self is not an object";
416 my $name = $AUTOLOAD;
419 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
420 $logger->error("circulator: $type: invalid autoload field: $name");
421 die "$type: invalid autoload field: $name\n"
426 *{"${type}::${name}"} = sub {
429 $s->{$name} = $v if defined $v;
433 return $self->$name($data);
438 my( $class, $auth, %args ) = @_;
439 $class = ref($class) || $class;
440 my $self = bless( {}, $class );
443 $self->editor(new_editor(xact => 1, authtoken => $auth));
445 unless( $self->editor->checkauth ) {
446 $self->bail_on_events($self->editor->event);
450 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
452 $self->$_($args{$_}) for keys %args;
455 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
457 # if this is a renewal, default to desk_renewal
458 $self->desk_renewal(1) unless
459 $self->opac_renewal or $self->phone_renewal;
461 $self->capture('') unless $self->capture;
463 unless(%user_groups) {
464 my $gps = $self->editor->retrieve_all_permission_grp_tree;
465 %user_groups = map { $_->id => $_ } @$gps;
472 # --------------------------------------------------------------------------
473 # True if we should discontinue processing
474 # --------------------------------------------------------------------------
476 my( $self, $bool ) = @_;
477 if( defined $bool ) {
478 $logger->info("circulator: BAILING OUT") if $bool;
479 $self->{bail_out} = $bool;
481 return $self->{bail_out};
486 my( $self, @evts ) = @_;
489 $logger->info("circulator: pushing event ".$e->{textcode});
490 push( @{$self->events}, $e ) unless
491 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
497 my $key = md5_hex( time() . rand() . "$$" );
498 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
499 return $self->permit_key($key);
502 sub check_permit_key {
504 my $key = $self->permit_key;
505 return 0 unless $key;
506 my $k = "oils_permit_key_$key";
507 my $one = $self->cache_handle->get_cache($k);
508 $self->cache_handle->delete_cache($k);
509 return ($one) ? 1 : 0;
514 my $e = $self->editor;
516 # --------------------------------------------------------------------------
517 # Grab the fleshed copy
518 # --------------------------------------------------------------------------
519 unless($self->is_noncat) {
523 flesh_fields => {acp => ['call_number'], acn => ['record']}
526 $copy = $e->retrieve_asset_copy(
527 [$self->copy_id, $flesh ]) or return $e->event;
529 } elsif( $self->copy_barcode ) {
531 $copy = $e->search_asset_copy(
532 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
537 $self->volume($copy->call_number);
538 $self->title($self->volume->record);
539 $self->copy->call_number($self->volume->id);
540 $self->volume->record($self->title->id);
541 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
542 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
543 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
544 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
547 # We can't renew if there is no copy
548 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
549 if $self->is_renewal;
554 # --------------------------------------------------------------------------
556 # --------------------------------------------------------------------------
558 if( $self->patron_id ) {
559 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
561 } elsif( $self->patron_barcode ) {
563 my $card = $e->search_actor_card(
564 {barcode => $self->patron_barcode})->[0] or return $e->event;
566 $patron = $e->search_actor_user(
567 {card => $card->id})->[0] or return $e->event;
570 if( my $copy = $self->copy ) {
571 my $circs = $e->search_action_circulation(
572 {target_copy => $copy->id, checkin_time => undef});
574 if( my $circ = $circs->[0] ) {
575 $patron = $e->retrieve_actor_user($circ->usr)
581 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
582 unless $self->patron($patron) or $self->is_checkin;
585 # --------------------------------------------------------------------------
586 # This builds the script runner environment and fetches most of the
588 # --------------------------------------------------------------------------
589 sub mk_script_runner {
595 qw/copy copy_barcode copy_id patron
596 patron_id patron_barcode volume title editor/;
598 # Translate our objects into the ScriptBuilder args hash
599 $$args{$_} = $self->$_() for @fields;
601 $args->{ignore_user_status} = 1 if $self->is_checkin;
602 $$args{fetch_patron_by_circ_copy} = 1;
603 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
605 if( my $pco = $self->pending_checkouts ) {
606 $logger->info("circulator: we were given a pending checkouts number of $pco");
607 $$args{patronItemsOut} = $pco;
610 # This fetches most of the objects we need
611 $self->script_runner(
612 OpenILS::Application::Circ::ScriptBuilder->build($args));
614 # Now we translate the ScriptBuilder objects back into self
615 $self->$_($$args{$_}) for @fields;
617 my @evts = @{$args->{_events}} if $args->{_events};
619 $logger->debug("circulator: script builder returned events: @evts") if @evts;
623 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
624 if(!$self->is_noncat and
626 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
630 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
631 return $self->bail_on_events(@e);
636 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
637 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
638 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
639 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
643 # We can't renew if there is no copy
644 return $self->bail_on_events(@evts) if
645 $self->is_renewal and !$self->copy;
647 # Set some circ-specific flags in the script environment
648 my $evt = "environment";
649 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
651 if( $self->is_noncat ) {
652 $self->script_runner->insert("$evt.isNonCat", 1);
653 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
656 if( $self->is_precat ) {
657 $self->script_runner->insert("environment.isPrecat", 1, 1);
660 $self->script_runner->add_path( $_ ) for @$script_libs;
665 # --------------------------------------------------------------------------
666 # Does the circ permit work
667 # --------------------------------------------------------------------------
671 $self->log_me("do_permit()");
673 unless( $self->editor->requestor->id == $self->patron->id ) {
674 return $self->bail_on_events($self->editor->event)
675 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
678 $self->check_captured_holds();
679 $self->do_copy_checks();
680 return if $self->bail_out;
681 $self->run_patron_permit_scripts();
682 $self->run_copy_permit_scripts()
683 unless $self->is_precat or $self->is_noncat;
684 $self->check_item_deposit_events();
685 $self->override_events();
686 return if $self->bail_out;
688 if($self->is_precat and not $self->request_precat) {
691 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
692 return $self->bail_out(1) unless $self->is_renewal;
696 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
699 sub check_item_deposit_events {
701 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
702 if $self->is_deposit and not $self->is_deposit_exempt;
703 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
704 if $self->is_rental and not $self->is_rental_exempt;
707 # returns true if the user is not required to pay deposits
708 sub is_deposit_exempt {
710 my $pid = (ref $self->patron->profile) ?
711 $self->patron->profile->id : $self->patron->profile;
712 my $groups = $U->ou_ancestor_setting_value(
713 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
714 for my $grp (@$groups) {
715 return 1 if $self->is_group_descendant($grp, $pid);
720 # returns true if the user is not required to pay rental fees
721 sub is_rental_exempt {
723 my $pid = (ref $self->patron->profile) ?
724 $self->patron->profile->id : $self->patron->profile;
725 my $groups = $U->ou_ancestor_setting_value(
726 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
727 for my $grp (@$groups) {
728 return 1 if $self->is_group_descendant($grp, $pid);
733 sub is_group_descendant {
734 my($self, $p_id, $c_id) = @_;
735 return 0 unless defined $p_id and defined $c_id;
736 return 1 if $c_id == $p_id;
737 while(my $grp = $user_groups{$c_id}) {
738 $c_id = $grp->parent;
739 return 0 unless defined $c_id;
740 return 1 if $c_id == $p_id;
745 sub check_captured_holds {
747 my $copy = $self->copy;
748 my $patron = $self->patron;
750 return undef unless $copy;
752 my $s = $U->copy_status($copy->status)->id;
753 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
754 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
756 # Item is on the holds shelf, make sure it's going to the right person
757 my $holds = $self->editor->search_action_hold_request(
760 current_copy => $copy->id ,
761 capture_time => { '!=' => undef },
762 cancel_time => undef,
763 fulfillment_time => undef
769 if( $holds and $$holds[0] ) {
770 return undef if $$holds[0]->usr == $patron->id;
773 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
775 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
781 my $copy = $self->copy;
784 my $stat = $U->copy_status($copy->status)->id;
786 # We cannot check out a copy if it is in-transit
787 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
788 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
791 $self->handle_claims_returned();
792 return if $self->bail_out;
794 # no claims returned circ was found, check if there is any open circ
795 unless( $self->is_renewal ) {
797 my $circs = $self->editor->search_action_circulation(
798 { target_copy => $copy->id, checkin_time => undef }
801 if(my $old_circ = $circs->[0]) { # an open circ was found
803 my $payload; # event payload
805 if($old_circ->usr == $self->patron->id) {
807 $payload = {old_circ => $old_circ};
809 # If there is an open circulation on the checkout item and an auto-renew
810 # interval is defined, inform the caller that they should go
811 # ahead and renew the item instead of warning about open circulations.
813 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
814 $self->editor->requestor->ws_ou,
815 'circ.checkout_auto_renew_age',
819 if($auto_renew_intvl) {
820 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
821 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($old_circ->xact_start) );
823 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
824 $payload->{auto_renew} = 1;
829 return $self->bail_on_events(
830 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
836 my $LEGACY_CIRC_EVENT_MAP = {
837 'actor.usr.barred' => 'PATRON_BARRED',
838 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
839 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
840 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
841 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
842 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
843 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
844 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
845 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
849 # ---------------------------------------------------------------------
850 # This pushes any patron-related events into the list but does not
851 # set bail_out for any events
852 # ---------------------------------------------------------------------
853 sub run_patron_permit_scripts {
855 my $runner = $self->script_runner;
856 my $patronid = $self->patron->id;
860 if(!$self->legacy_script_support) {
862 my $results = $self->run_indb_circ_test;
863 unless($self->circ_test_success) {
864 push(@allevents, OpenILS::Event->new(
865 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
871 # ---------------------------------------------------------------------
872 # # Now run the patron permit script
873 # ---------------------------------------------------------------------
874 $runner->load($self->circ_permit_patron);
875 my $result = $runner->run or
876 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
878 my $patron_events = $result->{events};
880 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
881 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
882 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
883 $penalties = $penalties->{fatal_penalties};
885 for my $pen (@$penalties) {
886 my $event = OpenILS::Event->new($pen->name);
887 $event->{desc} = $pen->label;
888 push(@allevents, $event);
891 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
894 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
896 $self->push_events(@allevents);
899 sub run_indb_circ_test {
901 return $self->matrix_test_result if $self->matrix_test_result;
903 my $dbfunc = ($self->is_renewal) ?
904 'action.item_user_renew_test' : 'action.item_user_circ_test';
906 my $results = $self->editor->json_query(
909 $self->editor->requestor->ws_ou,
910 ($self->is_precat or $self->is_noncat) ? undef : $self->copy->id,
916 $self->circ_test_success($U->is_true($results->[0]->{success}));
918 if(my $mp = $results->[0]->{matchpoint}) {
919 $self->circ_matrix_matchpoint(
920 $self->editor->retrieve_config_circ_matrix_matchpoint([
923 flesh_fields => {ccmm =>
924 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
930 return $self->matrix_test_result($results);
933 # ---------------------------------------------------------------------
934 # given a use and copy, this will calculate the circulation policy
935 # parameters. Only works with in-db circ.
936 # ---------------------------------------------------------------------
940 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
942 $self->run_indb_circ_test;
945 circ_test_success => $self->circ_test_success,
946 failure_events => [],
950 unless($self->circ_test_success) {
951 push(@{$results->{failure_codes}},
952 $_->{fail_part}) for @{$self->matrix_test_result};
953 push(@{$results->{failure_events}},
954 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part})
955 for @{$self->matrix_test_result};
958 if($self->circ_matrix_matchpoint) {
959 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
960 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
961 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
963 my $policy = $self->get_circ_policy(
964 $duration_rule, $recurring_fine_rule, $max_fine_rule);
966 $$results{$_} = $$policy{$_} for keys %$policy;
972 # ---------------------------------------------------------------------
973 # Loads the circ policy info for duration, recurring fine, and max
974 # fine based on the current copy
975 # ---------------------------------------------------------------------
976 sub get_circ_policy {
977 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
980 duration_rule => $duration_rule->name,
981 recurring_fine_rule => $recurring_fine_rule->name,
982 max_fine_rule => $max_fine_rule->name,
983 max_fine => $self->get_max_fine_amount($max_fine_rule),
984 fine_interval => $recurring_fine_rule->recurance_interval,
985 renewal_remaining => $duration_rule->max_renewals
988 $policy->{duration} = $duration_rule->shrt
989 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
990 $policy->{duration} = $duration_rule->normal
991 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
992 $policy->{duration} = $duration_rule->extended
993 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
995 $policy->{recurring_fine} = $recurring_fine_rule->low
996 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
997 $policy->{recurring_fine} = $recurring_fine_rule->normal
998 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
999 $policy->{recurring_fine} = $recurring_fine_rule->high
1000 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1005 sub get_max_fine_amount {
1007 my $max_fine_rule = shift;
1008 my $max_amount = $max_fine_rule->amount;
1010 # if is_percent is true then the max->amount is
1011 # use as a percentage of the copy price
1012 if ($U->is_true($max_fine_rule->is_percent)) {
1013 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1014 $max_amount = $price * $max_fine_rule->amount / 100;
1022 sub run_copy_permit_scripts {
1024 my $copy = $self->copy || return;
1025 my $runner = $self->script_runner;
1029 if(!$self->legacy_script_support) {
1030 my $results = $self->run_indb_circ_test;
1031 unless($self->circ_test_success) {
1032 push(@allevents, OpenILS::Event->new(
1033 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
1038 # ---------------------------------------------------------------------
1039 # Capture all of the copy permit events
1040 # ---------------------------------------------------------------------
1041 $runner->load($self->circ_permit_copy);
1042 my $result = $runner->run or
1043 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1044 my $copy_events = $result->{events};
1046 # ---------------------------------------------------------------------
1047 # Now collect all of the events together
1048 # ---------------------------------------------------------------------
1049 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1052 # See if this copy has an alert message
1053 my $ae = $self->check_copy_alert();
1054 push( @allevents, $ae ) if $ae;
1056 # uniquify the events
1057 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1058 @allevents = values %hash;
1061 $_->{payload} = $copy if
1062 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1065 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1067 $self->push_events(@allevents);
1071 sub check_copy_alert {
1073 return undef if $self->is_renewal;
1074 return OpenILS::Event->new(
1075 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1076 if $self->copy and $self->copy->alert_message;
1082 # --------------------------------------------------------------------------
1083 # If the call is overriding and has permissions to override every collected
1084 # event, the are cleared. Any event that the caller does not have
1085 # permission to override, will be left in the event list and bail_out will
1087 # XXX We need code in here to cancel any holds/transits on copies
1088 # that are being force-checked out
1089 # --------------------------------------------------------------------------
1090 sub override_events {
1092 my @events = @{$self->events};
1093 return unless @events;
1095 if(!$self->override) {
1096 return $self->bail_out(1)
1097 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1102 for my $e (@events) {
1103 my $tc = $e->{textcode};
1104 next if $tc eq 'SUCCESS';
1105 my $ov = "$tc.override";
1106 $logger->info("circulator: attempting to override event: $ov");
1108 return $self->bail_on_events($self->editor->event)
1109 unless( $self->editor->allowed($ov) );
1114 # --------------------------------------------------------------------------
1115 # If there is an open claimsreturn circ on the requested copy, close the
1116 # circ if overriding, otherwise bail out
1117 # --------------------------------------------------------------------------
1118 sub handle_claims_returned {
1120 my $copy = $self->copy;
1122 my $CR = $self->editor->search_action_circulation(
1124 target_copy => $copy->id,
1125 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1126 checkin_time => undef,
1130 return unless ($CR = $CR->[0]);
1134 # - If the caller has set the override flag, we will check the item in
1135 if($self->override) {
1137 $CR->checkin_time('now');
1138 $CR->checkin_scan_time('now');
1139 $CR->checkin_lib($self->editor->requestor->ws_ou);
1140 $CR->checkin_workstation($self->editor->requestor->wsid);
1141 $CR->checkin_staff($self->editor->requestor->id);
1143 $evt = $self->editor->event
1144 unless $self->editor->update_action_circulation($CR);
1147 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1150 $self->bail_on_events($evt) if $evt;
1155 # --------------------------------------------------------------------------
1156 # This performs the checkout
1157 # --------------------------------------------------------------------------
1161 $self->log_me("do_checkout()");
1163 # make sure perms are good if this isn't a renewal
1164 unless( $self->is_renewal ) {
1165 return $self->bail_on_events($self->editor->event)
1166 unless( $self->editor->allowed('COPY_CHECKOUT') );
1169 # verify the permit key
1170 unless( $self->check_permit_key ) {
1171 if( $self->permit_override ) {
1172 return $self->bail_on_events($self->editor->event)
1173 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1175 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1179 # if this is a non-cataloged circ, build the circ and finish
1180 if( $self->is_noncat ) {
1181 $self->checkout_noncat;
1183 OpenILS::Event->new('SUCCESS',
1184 payload => { noncat_circ => $self->circ }));
1188 if( $self->is_precat ) {
1189 $self->make_precat_copy;
1190 return if $self->bail_out;
1192 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1193 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1196 $self->do_copy_checks;
1197 return if $self->bail_out;
1199 $self->run_checkout_scripts();
1200 return if $self->bail_out;
1202 $self->build_checkout_circ_object();
1203 return if $self->bail_out;
1205 $self->apply_modified_due_date();
1206 return if $self->bail_out;
1208 return $self->bail_on_events($self->editor->event)
1209 unless $self->editor->create_action_circulation($self->circ);
1211 # refresh the circ to force local time zone for now
1212 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1214 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1216 return if $self->bail_out;
1218 $self->apply_deposit_fee();
1219 return if $self->bail_out;
1221 $self->handle_checkout_holds();
1222 return if $self->bail_out;
1224 # ------------------------------------------------------------------------------
1225 # Update the patron penalty info in the DB. Run it for permit-overrides
1226 # since the penalties are not updated during the permit phase
1227 # ------------------------------------------------------------------------------
1228 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1230 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1233 if($self->is_renewal) {
1234 # flesh the billing summary for the checked-in circ
1235 $pcirc = $self->editor->retrieve_action_circulation([
1237 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1242 OpenILS::Event->new('SUCCESS',
1244 copy => $U->unflesh_copy($self->copy),
1245 circ => $self->circ,
1247 holds_fulfilled => $self->fulfilled_holds,
1248 deposit_billing => $self->deposit_billing,
1249 rental_billing => $self->rental_billing,
1250 parent_circ => $pcirc
1256 sub apply_deposit_fee {
1258 my $copy = $self->copy;
1260 ($self->is_deposit and not $self->is_deposit_exempt) or
1261 ($self->is_rental and not $self->is_rental_exempt);
1263 my $bill = Fieldmapper::money::billing->new;
1264 my $amount = $copy->deposit_amount;
1268 if($self->is_deposit) {
1269 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1271 $self->deposit_billing($bill);
1273 $billing_type = OILS_BILLING_TYPE_RENTAL;
1275 $self->rental_billing($bill);
1278 $bill->xact($self->circ->id);
1279 $bill->amount($amount);
1280 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1281 $bill->billing_type($billing_type);
1282 $bill->btype($btype);
1283 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1285 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1290 my $copy = $self->copy;
1292 my $stat = $copy->status if ref $copy->status;
1293 my $loc = $copy->location if ref $copy->location;
1294 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1296 $copy->status($stat->id) if $stat;
1297 $copy->location($loc->id) if $loc;
1298 $copy->circ_lib($circ_lib->id) if $circ_lib;
1299 $copy->editor($self->editor->requestor->id);
1300 $copy->edit_date('now');
1301 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1303 return $self->bail_on_events($self->editor->event)
1304 unless $self->editor->update_asset_copy($self->copy);
1306 $copy->status($U->copy_status($copy->status));
1307 $copy->location($loc) if $loc;
1308 $copy->circ_lib($circ_lib) if $circ_lib;
1312 sub bail_on_events {
1313 my( $self, @evts ) = @_;
1314 $self->push_events(@evts);
1319 # ------------------------------------------------------------------------------
1320 # When an item is checked out, see if we can fulfill a hold for this patron
1321 # ------------------------------------------------------------------------------
1322 sub handle_checkout_holds {
1324 my $copy = $self->copy;
1325 my $patron = $self->patron;
1327 my $e = $self->editor;
1328 $self->fulfilled_holds([]);
1330 # pre/non-cats can't fulfill a hold
1331 return if $self->is_precat or $self->is_noncat;
1333 my $hold = $e->search_action_hold_request({
1334 current_copy => $copy->id ,
1335 cancel_time => undef,
1336 fulfillment_time => undef,
1338 {expire_time => undef},
1339 {expire_time => {'>' => 'now'}}
1343 if($hold and $hold->usr != $patron->id) {
1344 # reset the hold since the copy is now checked out
1346 $logger->info("circulator: un-targeting hold ".$hold->id.
1347 " because copy ".$copy->id." is getting checked out");
1349 $hold->clear_prev_check_time;
1350 $hold->clear_current_copy;
1351 $hold->clear_capture_time;
1353 return $self->bail_on_event($e->event)
1354 unless $e->update_action_hold_request($hold);
1360 $hold = $self->find_related_user_hold($copy, $patron) or return;
1361 $logger->info("circulator: found related hold to fulfill in checkout");
1364 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1366 # if the hold was never officially captured, capture it.
1367 $hold->current_copy($copy->id);
1368 $hold->capture_time('now') unless $hold->capture_time;
1369 $hold->fulfillment_time('now');
1370 $hold->fulfillment_staff($e->requestor->id);
1371 $hold->fulfillment_lib($e->requestor->ws_ou);
1373 return $self->bail_on_events($e->event)
1374 unless $e->update_action_hold_request($hold);
1376 $holdcode->delete_hold_copy_maps($e, $hold->id);
1377 return $self->fulfilled_holds([$hold->id]);
1381 # ------------------------------------------------------------------------------
1382 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1383 # the patron directly targets the checked out item, see if there is another hold
1384 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1385 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1386 # ------------------------------------------------------------------------------
1387 sub find_related_user_hold {
1388 my($self, $copy, $patron) = @_;
1389 my $e = $self->editor;
1391 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1393 return undef unless $U->ou_ancestor_setting_value(
1394 $e->requestor->ws_ou, 'circ.checkout_fills_related_hold', $e);
1396 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1398 select => {ahr => ['id']},
1403 fkey => 'current_copy',
1404 type => 'left' # there may be no current_copy
1411 fulfillment_time => undef,
1412 cancel_time => undef,
1414 {expire_time => undef},
1415 {expire_time => {'>' => 'now'}}
1422 target => $self->volume->id
1428 target => $self->title->id
1434 {id => undef}, # left-join copy may be nonexistent
1435 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1439 order_by => {ahr => {request_time => {direction => 'asc'}}},
1443 my $hold_info = $e->json_query($args)->[0];
1444 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1449 sub run_checkout_scripts {
1453 my $runner = $self->script_runner;
1462 if(!$self->legacy_script_support) {
1463 $self->run_indb_circ_test();
1464 $duration = $self->circ_matrix_matchpoint->duration_rule;
1465 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1466 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1470 $runner->load($self->circ_duration);
1472 my $result = $runner->run or
1473 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1475 $duration_name = $result->{durationRule};
1476 $recurring_name = $result->{recurringFinesRule};
1477 $max_fine_name = $result->{maxFine};
1480 $duration_name = $duration->name if $duration;
1481 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1484 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1485 return $self->bail_on_events($evt) if $evt;
1487 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1488 return $self->bail_on_events($evt) if $evt;
1490 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1491 return $self->bail_on_events($evt) if $evt;
1496 # The item circulates with an unlimited duration
1502 $self->duration_rule($duration);
1503 $self->recurring_fines_rule($recurring);
1504 $self->max_fine_rule($max_fine);
1508 sub build_checkout_circ_object {
1511 my $circ = Fieldmapper::action::circulation->new;
1512 my $duration = $self->duration_rule;
1513 my $max = $self->max_fine_rule;
1514 my $recurring = $self->recurring_fines_rule;
1515 my $copy = $self->copy;
1516 my $patron = $self->patron;
1520 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1522 my $dname = $duration->name;
1523 my $mname = $max->name;
1524 my $rname = $recurring->name;
1526 $logger->debug("circulator: building circulation ".
1527 "with duration=$dname, maxfine=$mname, recurring=$rname");
1529 $circ->duration($policy->{duration});
1530 $circ->recuring_fine($policy->{recurring_fine});
1531 $circ->duration_rule($duration->name);
1532 $circ->recuring_fine_rule($recurring->name);
1533 $circ->max_fine_rule($max->name);
1534 $circ->max_fine($policy->{max_fine});
1535 $circ->fine_interval($recurring->recurance_interval);
1536 $circ->renewal_remaining($duration->max_renewals);
1540 $logger->info("circulator: copy found with an unlimited circ duration");
1541 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1542 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1543 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1544 $circ->renewal_remaining(0);
1547 $circ->target_copy( $copy->id );
1548 $circ->usr( $patron->id );
1549 $circ->circ_lib( $self->circ_lib );
1550 $circ->workstation($self->editor->requestor->wsid)
1551 if defined $self->editor->requestor->wsid;
1553 # renewals maintain a link to the parent circulation
1554 $circ->parent_circ($self->parent_circ);
1556 if( $self->is_renewal ) {
1557 $circ->opac_renewal('t') if $self->opac_renewal;
1558 $circ->phone_renewal('t') if $self->phone_renewal;
1559 $circ->desk_renewal('t') if $self->desk_renewal;
1560 $circ->renewal_remaining($self->renewal_remaining);
1561 $circ->circ_staff($self->editor->requestor->id);
1565 # if the user provided an overiding checkout time,
1566 # (e.g. the checkout really happened several hours ago), then
1567 # we apply that here. Does this need a perm??
1568 $circ->xact_start(clense_ISO8601($self->checkout_time))
1569 if $self->checkout_time;
1571 # if a patron is renewing, 'requestor' will be the patron
1572 $circ->circ_staff($self->editor->requestor->id);
1573 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1579 sub apply_modified_due_date {
1581 my $circ = $self->circ;
1582 my $copy = $self->copy;
1584 if( $self->due_date ) {
1586 return $self->bail_on_events($self->editor->event)
1587 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1589 $circ->due_date(clense_ISO8601($self->due_date));
1593 # if the due_date lands on a day when the location is closed
1594 return unless $copy and $circ->due_date;
1596 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1598 # due-date overlap should be determined by the location the item
1599 # is checked out from, not the owning or circ lib of the item
1600 my $org = $self->editor->requestor->ws_ou;
1602 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1603 " with an item due date of ".$circ->due_date );
1605 my $dateinfo = $U->storagereq(
1606 'open-ils.storage.actor.org_unit.closed_date.overlap',
1607 $org, $circ->due_date );
1610 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1611 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1613 # XXX make the behavior more dynamic
1614 # for now, we just push the due date to after the close date
1615 $circ->due_date($dateinfo->{end});
1622 sub create_due_date {
1623 my( $self, $duration ) = @_;
1625 # if there is a raw time component (e.g. from postgres),
1626 # turn it into an interval that interval_to_seconds can parse
1627 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1629 # for now, use the server timezone. TODO: use workstation org timezone
1630 my $due_date = DateTime->now(time_zone => 'local');
1632 # add the circ duration
1633 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
1635 # return ISO8601 time with timezone
1636 return $due_date->strftime('%FT%T%z');
1641 sub make_precat_copy {
1643 my $copy = $self->copy;
1646 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1648 $copy->editor($self->editor->requestor->id);
1649 $copy->edit_date('now');
1650 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
1651 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
1652 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
1653 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
1654 $self->update_copy();
1658 $logger->info("circulator: Creating a new precataloged ".
1659 "copy in checkout with barcode " . $self->copy_barcode);
1661 $copy = Fieldmapper::asset::copy->new;
1662 $copy->circ_lib($self->circ_lib);
1663 $copy->creator($self->editor->requestor->id);
1664 $copy->editor($self->editor->requestor->id);
1665 $copy->barcode($self->copy_barcode);
1666 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1667 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1668 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1670 $copy->dummy_title($self->dummy_title || "");
1671 $copy->dummy_author($self->dummy_author || "");
1672 $copy->dummy_isbn($self->dummy_isbn || "");
1673 $copy->circ_modifier($self->circ_modifier);
1676 # See if we need to override the circ_lib for the copy with a configured circ_lib
1677 # Setting is shortname of the org unit
1678 my $precat_circ_lib = $U->ou_ancestor_setting_value(
1679 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
1681 if($precat_circ_lib) {
1682 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
1685 $self->bail_on_events($self->editor->event);
1689 $copy->circ_lib($org->id);
1693 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1695 $self->push_events($self->editor->event);
1699 # this is a little bit of a hack, but we need to
1700 # get the copy into the script runner
1701 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1705 sub checkout_noncat {
1711 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1712 my $count = $self->noncat_count || 1;
1713 my $cotime = clense_ISO8601($self->checkout_time) || "";
1715 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
1719 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1720 $self->editor->requestor->id,
1728 $self->push_events($evt);
1739 $self->log_me("do_checkin()");
1741 return $self->bail_on_events(
1742 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1745 if( $self->checkin_check_holds_shelf() ) {
1746 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1747 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1748 $self->checkin_flesh_events;
1752 unless( $self->is_renewal ) {
1753 return $self->bail_on_events($self->editor->event)
1754 unless $self->editor->allowed('COPY_CHECKIN');
1757 $self->push_events($self->check_copy_alert());
1758 $self->push_events($self->check_checkin_copy_status());
1760 # the renew code will have already found our circulation object
1761 unless( $self->is_renewal and $self->circ ) {
1762 my $circs = $self->editor->search_action_circulation(
1763 { target_copy => $self->copy->id, checkin_time => undef });
1764 $self->circ($$circs[0]);
1766 # for now, just warn if there are multiple open circs on a copy
1767 $logger->warn("circulator: we have ".scalar(@$circs).
1768 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1771 # run the fine generator against this circ, if this circ is there
1772 $self->generate_fines if ($self->circ);
1774 # if the circ is marked as 'claims returned', add the event to the list
1775 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1776 if ($self->circ and $self->circ->stop_fines
1777 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1779 $self->check_circ_deposit();
1781 # handle the overridable events
1782 $self->override_events unless $self->is_renewal;
1783 return if $self->bail_out;
1787 $self->editor->search_action_transit_copy(
1788 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1792 $self->checkin_handle_circ;
1793 return if $self->bail_out;
1794 $self->checkin_changed(1);
1796 } elsif( $self->transit ) {
1797 my $hold_transit = $self->process_received_transit;
1798 $self->checkin_changed(1);
1800 if( $self->bail_out ) {
1801 $self->checkin_flesh_events;
1805 if( my $e = $self->check_checkin_copy_status() ) {
1806 # If the original copy status is special, alert the caller
1807 my $ev = $self->events;
1808 $self->events([$e]);
1809 $self->override_events;
1810 return if $self->bail_out;
1814 if( $hold_transit or
1815 $U->copy_status($self->copy->status)->id
1816 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1819 if( $hold_transit ) {
1820 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1822 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1827 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1829 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1830 $self->reshelve_copy(1);
1831 $self->cancelled_hold_transit(1);
1832 $self->notify_hold(0); # don't notify for cancelled holds
1833 return if $self->bail_out;
1837 # hold transited to correct location
1838 $self->checkin_flesh_events;
1843 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1845 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1846 " that is in-transit, but there is no transit.. repairing");
1847 $self->reshelve_copy(1);
1848 return if $self->bail_out;
1851 if( $self->is_renewal ) {
1852 $self->push_events(OpenILS::Event->new('SUCCESS'));
1856 # ------------------------------------------------------------------------------
1857 # Circulations and transits are now closed where necessary. Now go on to see if
1858 # this copy can fulfill a hold or needs to be routed to a different location
1859 # ------------------------------------------------------------------------------
1861 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1863 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1864 return if $self->bail_out;
1866 unless($needed_for_hold) {
1867 my $circ_lib = (ref $self->copy->circ_lib) ?
1868 $self->copy->circ_lib->id : $self->copy->circ_lib;
1870 if( $self->remote_hold ) {
1871 $circ_lib = $self->remote_hold->pickup_lib;
1872 $logger->warn("circulator: Copy ".$self->copy->barcode.
1873 " is on a remote hold's shelf, sending to $circ_lib");
1876 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1878 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1880 $self->checkin_handle_precat();
1881 return if $self->bail_out;
1885 my $bc = $self->copy->barcode;
1886 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1887 $self->checkin_build_copy_transit($circ_lib);
1888 return if $self->bail_out;
1889 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1894 $self->reshelve_copy;
1895 return if $self->bail_out;
1897 unless($self->checkin_changed) {
1899 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1900 my $stat = $U->copy_status($self->copy->status)->id;
1902 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1903 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1904 $self->bail_out(1); # no need to commit anything
1908 $self->push_events(OpenILS::Event->new('SUCCESS'))
1909 unless @{$self->events};
1912 OpenILS::Utils::Penalty->calculate_penalties(
1913 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1915 $self->checkin_flesh_events;
1919 # if a deposit was payed for this item, push the event
1920 sub check_circ_deposit {
1922 return unless $self->circ;
1923 my $deposit = $self->editor->search_money_billing(
1925 xact => $self->circ->id,
1927 }, {idlist => 1})->[0];
1929 $self->push_events(OpenILS::Event->new(
1930 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1935 my $force = $self->force || shift;
1936 my $copy = $self->copy;
1938 my $stat = $U->copy_status($copy->status)->id;
1941 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1942 $stat != OILS_COPY_STATUS_CATALOGING and
1943 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1944 $stat != OILS_COPY_STATUS_RESHELVING )) {
1946 $copy->status( OILS_COPY_STATUS_RESHELVING );
1948 $self->checkin_changed(1);
1953 # Returns true if the item is at the current location
1954 # because it was transited there for a hold and the
1955 # hold has not been fulfilled
1956 sub checkin_check_holds_shelf {
1958 return 0 unless $self->copy;
1961 $U->copy_status($self->copy->status)->id ==
1962 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1964 # find the hold that put us on the holds shelf
1965 my $holds = $self->editor->search_action_hold_request(
1967 current_copy => $self->copy->id,
1968 capture_time => { '!=' => undef },
1969 fulfillment_time => undef,
1970 cancel_time => undef,
1975 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1976 $self->reshelve_copy(1);
1980 my $hold = $$holds[0];
1982 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1983 $hold->id. "] for copy ".$self->copy->barcode);
1985 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1986 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1990 $logger->info("circulator: hold is not for here..");
1991 $self->remote_hold($hold);
1996 sub checkin_handle_precat {
1998 my $copy = $self->copy;
2000 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2001 $copy->status(OILS_COPY_STATUS_CATALOGING);
2002 $self->update_copy();
2003 $self->checkin_changed(1);
2004 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2009 sub checkin_build_copy_transit {
2012 my $copy = $self->copy;
2013 my $transit = Fieldmapper::action::transit_copy->new;
2015 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2016 $logger->info("circulator: transiting copy to $dest");
2018 $transit->source($self->editor->requestor->ws_ou);
2019 $transit->dest($dest);
2020 $transit->target_copy($copy->id);
2021 $transit->source_send_time('now');
2022 $transit->copy_status( $U->copy_status($copy->status)->id );
2024 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2026 return $self->bail_on_events($self->editor->event)
2027 unless $self->editor->create_action_transit_copy($transit);
2029 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2031 $self->checkin_changed(1);
2035 # returns true if the item was used (or may potentially be used
2036 # in subsequent calls) to capture a hold.
2037 sub attempt_checkin_hold_capture {
2039 my $copy = $self->copy;
2041 # we've been explicitly told not to capture any holds
2042 return 0 if $self->capture eq 'nocapture';
2044 # See if this copy can fulfill any holds
2045 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2046 $self->editor, $copy, $self->editor->requestor );
2049 $logger->debug("circulator: no potential permitted".
2050 "holds found for copy ".$copy->barcode);
2054 if($self->capture ne 'capture') {
2055 # see if this item is in a hold-capture-delay location
2056 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2057 if($U->is_true($location->hold_verify)) {
2058 $self->bail_on_events(
2059 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2064 $self->retarget($retarget);
2066 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2068 $hold->current_copy($copy->id);
2069 $hold->capture_time('now');
2070 $hold->shelf_time('now')
2071 if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2073 # prevent DB errors caused by fetching
2074 # holds from storage, and updating through cstore
2075 $hold->clear_fulfillment_time;
2076 $hold->clear_fulfillment_staff;
2077 $hold->clear_fulfillment_lib;
2078 $hold->clear_expire_time;
2079 $hold->clear_cancel_time;
2080 $hold->clear_prev_check_time unless $hold->prev_check_time;
2082 $self->bail_on_events($self->editor->event)
2083 unless $self->editor->update_action_hold_request($hold);
2085 $self->checkin_changed(1);
2087 return 0 if $self->bail_out;
2089 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2091 # This hold was captured in the correct location
2092 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2093 $self->push_events(OpenILS::Event->new('SUCCESS'));
2095 #$self->do_hold_notify($hold->id);
2096 $self->notify_hold($hold->id);
2100 # Hold needs to be picked up elsewhere. Build a hold
2101 # transit and route the item.
2102 $self->checkin_build_hold_transit();
2103 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2104 return 0 if $self->bail_out;
2105 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2108 # make sure we save the copy status
2113 sub do_hold_notify {
2114 my( $self, $holdid ) = @_;
2116 my $e = new_editor(xact => 1);
2117 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2119 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2120 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2122 $logger->info("circulator: running delayed hold notify process");
2124 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2125 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2127 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2128 hold_id => $holdid, requestor => $self->editor->requestor);
2130 $logger->debug("circulator: built hold notifier");
2132 if(!$notifier->event) {
2134 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2136 my $stat = $notifier->send_email_notify;
2137 if( $stat == '1' ) {
2138 $logger->info("circulator: hold notify succeeded for hold $holdid");
2142 $logger->warn("circulator: * hold notify failed for hold $holdid");
2145 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2149 sub retarget_holds {
2151 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2152 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2153 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2154 # no reason to wait for the return value
2158 sub checkin_build_hold_transit {
2161 my $copy = $self->copy;
2162 my $hold = $self->hold;
2163 my $trans = Fieldmapper::action::hold_transit_copy->new;
2165 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2167 $trans->hold($hold->id);
2168 $trans->source($self->editor->requestor->ws_ou);
2169 $trans->dest($hold->pickup_lib);
2170 $trans->source_send_time("now");
2171 $trans->target_copy($copy->id);
2173 # when the copy gets to its destination, it will recover
2174 # this status - put it onto the holds shelf
2175 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2177 return $self->bail_on_events($self->editor->event)
2178 unless $self->editor->create_action_hold_transit_copy($trans);
2183 sub process_received_transit {
2185 my $copy = $self->copy;
2186 my $copyid = $self->copy->id;
2188 my $status_name = $U->copy_status($copy->status)->name;
2189 $logger->debug("circulator: attempting transit receive on ".
2190 "copy $copyid. Copy status is $status_name");
2192 my $transit = $self->transit;
2194 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2195 # - this item is in-transit to a different location
2197 my $tid = $transit->id;
2198 my $loc = $self->editor->requestor->ws_ou;
2199 my $dest = $transit->dest;
2201 $logger->info("circulator: Fowarding transit on copy which is destined ".
2202 "for a different location. transit=$tid, copy=$copyid, current ".
2203 "location=$loc, destination location=$dest");
2205 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2207 # grab the associated hold object if available
2208 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2209 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2211 return $self->bail_on_events($evt);
2214 # The transit is received, set the receive time
2215 $transit->dest_recv_time('now');
2216 $self->bail_on_events($self->editor->event)
2217 unless $self->editor->update_action_transit_copy($transit);
2219 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2221 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2222 $copy->status( $transit->copy_status );
2223 $self->update_copy();
2224 return if $self->bail_out;
2228 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2230 # hold has arrived at destination, set shelf time
2231 $hold->shelf_time('now');
2232 $self->bail_on_events($self->editor->event)
2233 unless $self->editor->update_action_hold_request($hold);
2234 return if $self->bail_out;
2236 $self->notify_hold($hold_transit->hold);
2241 OpenILS::Event->new(
2244 payload => { transit => $transit, holdtransit => $hold_transit } ));
2246 return $hold_transit;
2250 sub generate_fines {
2255 my $st = OpenSRF::AppSession->connect('open-ils.storage');
2258 'open-ils.storage.action.circulation.overdue.generate_fines',
2265 # refresh the circ in case the fine generator set the stop_fines field
2266 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
2271 sub checkin_handle_circ {
2273 my $circ = $self->circ;
2274 my $copy = $self->copy;
2278 # backdate the circ if necessary
2279 if($self->backdate) {
2280 $self->checkin_handle_backdate;
2281 return if $self->bail_out;
2284 if($self->void_overdues) {
2285 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2286 $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2287 return $self->bail_on_events($evt) if $evt;
2290 if(!$circ->stop_fines) {
2291 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2292 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2293 $circ->stop_fines_time('now') unless $self->backdate;
2294 $circ->stop_fines_time($self->backdate) if $self->backdate;
2297 # see if there are any fines owed on this circ. if not, close it
2298 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2299 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2301 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2303 # Set the checkin vars since we have the item
2304 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2306 # capture the true scan time for back-dated checkins
2307 $circ->checkin_scan_time('now');
2309 $circ->checkin_staff($self->editor->requestor->id);
2310 $circ->checkin_lib($self->editor->requestor->ws_ou);
2311 $circ->checkin_workstation($self->editor->requestor->wsid);
2313 my $circ_lib = (ref $self->copy->circ_lib) ?
2314 $self->copy->circ_lib->id : $self->copy->circ_lib;
2315 my $stat = $U->copy_status($self->copy->status)->id;
2317 # immediately available keeps items lost or missing items from going home before being handled
2318 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2319 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2322 if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2324 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2325 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2327 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2331 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2333 $self->checkin_handle_lost($circ_lib);
2337 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2341 return $self->bail_on_events($self->editor->event)
2342 unless $self->editor->update_action_circulation($circ);
2344 # make sure the circ isn't closed if we just voided some fines
2345 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2346 return $self->bail_on_events($evt) if $evt;
2352 # ------------------------------------------------------------------
2353 # See if we need to void billings for lost checkin
2354 # ------------------------------------------------------------------
2355 sub checkin_handle_lost {
2357 my $circ_lib = shift;
2358 my $circ = $self->circ;
2360 my $max_return = $U->ou_ancestor_setting_value(
2361 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2366 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2367 $tm[5] -= 1 if $tm[5] > 0;
2368 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2370 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2371 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2373 $max_return = 0 if $today < $last_chance;
2376 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2378 my $void_lost = $U->ou_ancestor_setting_value(
2379 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2380 my $void_lost_fee = $U->ou_ancestor_setting_value(
2381 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2382 my $restore_od = $U->ou_ancestor_setting_value(
2383 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2385 $self->checkin_handle_lost_now_found(3) if $void_lost;
2386 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2387 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2390 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2395 sub checkin_handle_backdate {
2398 my $bd = $self->backdate;
2400 # ------------------------------------------------------------------
2401 # clean up the backdate for date comparison
2402 # we want any bills created on or after the backdate
2403 # ------------------------------------------------------------------
2404 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2405 #$bd = "${bd}T23:59:59";
2407 my $bills = $self->editor->search_money_billing(
2409 billing_ts => { '>=' => $bd },
2410 xact => $self->circ->id,
2415 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2417 for my $bill (@$bills) {
2418 unless( $U->is_true($bill->voided) ) {
2419 $logger->info("backdate voiding bill ".$bill->id);
2421 $bill->void_time('now');
2422 $bill->voider($self->editor->requestor->id);
2423 my $n = $bill->note || "";
2424 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2426 $self->bail_on_events($self->editor->event)
2427 unless $self->editor->update_money_billing($bill);
2435 sub find_patron_from_copy {
2437 my $circs = $self->editor->search_action_circulation(
2438 { target_copy => $self->copy->id, checkin_time => undef });
2439 my $circ = $circs->[0];
2440 return unless $circ;
2441 my $u = $self->editor->retrieve_actor_user($circ->usr)
2442 or return $self->bail_on_events($self->editor->event);
2446 sub check_checkin_copy_status {
2448 my $copy = $self->copy;
2454 my $status = $U->copy_status($copy->status)->id;
2457 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2458 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2459 $status == OILS_COPY_STATUS_IN_PROCESS ||
2460 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2461 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2462 $status == OILS_COPY_STATUS_CATALOGING ||
2463 $status == OILS_COPY_STATUS_RESHELVING );
2465 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2466 if( $status == OILS_COPY_STATUS_LOST );
2468 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2469 if( $status == OILS_COPY_STATUS_MISSING );
2471 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2476 # --------------------------------------------------------------------------
2477 # On checkin, we need to return as many relevant objects as we can
2478 # --------------------------------------------------------------------------
2479 sub checkin_flesh_events {
2482 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2483 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2484 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2487 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2490 if($self->hold and !$self->hold->cancel_time) {
2491 $hold = $self->hold;
2492 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
2496 # if we checked in a circulation, flesh the billing summary data
2497 $self->circ->billable_transaction(
2498 $self->editor->retrieve_money_billable_transaction([
2500 {flesh => 1, flesh_fields => {mbt => ['summary']}}
2506 # flesh some patron fields before returning
2508 $self->editor->retrieve_actor_user([
2513 au => ['card', 'billing_address', 'mailing_address']
2520 for my $evt (@{$self->events}) {
2523 $payload->{copy} = $U->unflesh_copy($self->copy);
2524 $payload->{record} = $record,
2525 $payload->{circ} = $self->circ;
2526 $payload->{transit} = $self->transit;
2527 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2528 $payload->{hold} = $hold;
2529 $payload->{patron} = $self->patron;
2530 $evt->{payload} = $payload;
2535 my( $self, $msg ) = @_;
2536 my $bc = ($self->copy) ? $self->copy->barcode :
2539 my $usr = ($self->patron) ? $self->patron->id : "";
2540 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2541 ", recipient=$usr, copy=$bc");
2547 $self->log_me("do_renew()");
2549 # Make sure there is an open circ to renew that is not
2550 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2551 my $usrid = $self->patron->id if $self->patron;
2554 # If we have a patron, match them to the circ
2555 $circ = $self->editor->search_action_circulation(
2556 {target_copy => $self->copy->id, usr => $usrid, stop_fines => undef})->[0];
2558 $circ = $self->editor->search_action_circulation(
2559 {target_copy => $self->copy->id, stop_fines => undef})->[0];
2564 $circ = $self->editor->search_action_circulation(
2565 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2567 $circ = $self->editor->search_action_circulation(
2568 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2572 return $self->bail_on_events($self->editor->event) unless $circ;
2574 # A user is not allowed to renew another user's items without permission
2575 unless( $circ->usr eq $self->editor->requestor->id ) {
2576 return $self->bail_on_events($self->editor->events)
2577 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2580 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2581 if $circ->renewal_remaining < 1;
2583 # -----------------------------------------------------------------
2585 $self->parent_circ($circ->id);
2586 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2589 $self->run_renew_permit;
2592 $self->do_checkin();
2593 return if $self->bail_out;
2595 unless( $self->permit_override ) {
2597 return if $self->bail_out;
2598 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2599 $self->remove_event('ITEM_NOT_CATALOGED');
2602 $self->override_events;
2603 return if $self->bail_out;
2606 $self->do_checkout();
2611 my( $self, $evt ) = @_;
2612 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2613 $logger->debug("circulator: removing event from list: $evt");
2614 my @events = @{$self->events};
2615 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2620 my( $self, $evt ) = @_;
2621 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2622 return grep { $_->{textcode} eq $evt } @{$self->events};
2627 sub run_renew_permit {
2632 if(!$self->legacy_script_support) {
2633 my $results = $self->run_indb_circ_test;
2634 unless($self->circ_test_success) {
2635 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) for @$results;
2640 my $runner = $self->script_runner;
2642 $runner->load($self->circ_permit_renew);
2643 my $result = $runner->run or
2644 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2645 $events = $result->{events};
2646 $self->mk_script_runner;
2649 $logger->activity("circulator: circ_permit_renew for user ".
2650 $self->patron->id." returned events: @$events") if @$events;
2652 $self->push_events(OpenILS::Event->new($_)) for @$events;
2654 $logger->debug("circulator: re-creating script runner to be safe");
2658 sub append_reading_list {
2662 $self->is_checkout and
2667 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2669 # verify history is globally enabled and uses the bucket mechanism
2670 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2671 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2673 unless($htype eq 'bucket') {
2678 # verify the patron wants to retain the hisory
2679 my $setting = $e->search_actor_user_setting(
2680 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2682 unless($setting and $setting->value) {
2687 my $bkt = $e->search_container_copy_bucket(
2688 {owner => $self->patron->id, btype => 'circ_history'})->[0];
2693 # find the next item position
2694 my $last_item = $e->search_container_copy_bucket_item(
2695 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2696 $pos = $last_item->pos + 1 if $last_item;
2699 # create the history bucket if necessary
2700 $bkt = Fieldmapper::container::copy_bucket->new;
2701 $bkt->owner($self->patron->id);
2703 $bkt->btype('circ_history');
2705 $e->create_container_copy_bucket($bkt) or return $e->die_event;
2708 my $item = Fieldmapper::container::copy_bucket_item->new;
2710 $item->bucket($bkt->id);
2711 $item->target_copy($self->copy->id);
2714 $e->create_container_copy_bucket_item($item) or return $e->die_event;
2721 sub make_trigger_events {
2723 return unless $self->circ;
2724 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2725 $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2726 $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2727 $ses->request('open-ils.trigger.event.autocreate', 'renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
2734 sub checkin_handle_lost_now_found {
2735 my ($self, $bill_type) = @_;
2737 # ------------------------------------------------------------------
2738 # remove charge from patron's account if lost item is returned
2739 # ------------------------------------------------------------------
2741 my $bills = $self->editor->search_money_billing(
2743 xact => $self->circ->id,
2748 $logger->debug("voiding lost item charge of ".scalar(@$bills));
2749 for my $bill (@$bills) {
2750 if( !$U->is_true($bill->voided) ) {
2751 $logger->info("lost item returned - voiding bill ".$bill->id);
2753 $bill->void_time('now');
2754 $bill->voider($self->editor->requestor->id);
2755 my $note = ($bill->note) ? $bill->note . "\n" : '';
2756 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2758 $self->bail_on_events($self->editor->event)
2759 unless $self->editor->update_money_billing($bill);
2764 sub checkin_handle_lost_now_found_restore_od {
2767 # ------------------------------------------------------------------
2768 # restore those overdue charges voided when item was set to lost
2769 # ------------------------------------------------------------------
2771 my $ods = $self->editor->search_money_billing(
2773 xact => $self->circ->id,
2778 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
2779 for my $bill (@$ods) {
2780 if( $U->is_true($bill->voided) ) {
2781 $logger->info("lost item returned - restoring overdue ".$bill->id);
2783 $bill->clear_void_time;
2784 $bill->voider($self->editor->requestor->id);
2785 my $note = ($bill->note) ? $bill->note . "\n" : '';
2786 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2788 $self->bail_on_events($self->editor->event)
2789 unless $self->editor->update_money_billing($bill);