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
118 __PACKAGE__->register_method(
119 method => "run_method",
120 api_name => "open-ils.circ.checkin.override",
121 signature => q/@see open-ils.circ.checkin/
124 __PACKAGE__->register_method(
125 method => "run_method",
126 api_name => "open-ils.circ.renew.override",
127 signature => q/@see open-ils.circ.renew/,
131 __PACKAGE__->register_method(
132 method => "run_method",
133 api_name => "open-ils.circ.renew",
134 notes => <<" NOTES");
135 PARAMS( authtoken, circ => circ_id );
136 open-ils.circ.renew(login_session, circ_object);
137 Renews the provided circulation. login_session is the requestor of the
138 renewal and if the logged in user is not the same as circ->usr, then
139 the logged in user must have RENEW_CIRC permissions.
142 __PACKAGE__->register_method(
143 method => "run_method",
144 api_name => "open-ils.circ.checkout.full");
145 __PACKAGE__->register_method(
146 method => "run_method",
147 api_name => "open-ils.circ.checkout.full.override");
149 __PACKAGE__->register_method(
150 method => "run_method",
151 api_name => "open-ils.circ.checkout.inspect",
153 Returns the circ matrix test result and, on success, the rule set and matrix test object
160 my( $self, $conn, $auth, $args ) = @_;
161 translate_legacy_args($args);
162 my $api = $self->api_name;
165 OpenILS::Application::Circ::Circulator->new($auth, %$args);
167 return circ_events($circulator) if $circulator->bail_out;
169 # --------------------------------------------------------------------------
170 # Go ahead and load the script runner to make sure we have all
171 # of the objects we need
172 # --------------------------------------------------------------------------
173 $circulator->is_renewal(1) if $api =~ /renew/;
174 $circulator->is_checkin(1) if $api =~ /checkin/;
176 if($legacy_script_support and not $circulator->is_checkin) {
177 $circulator->mk_script_runner();
178 $circulator->legacy_script_support(1);
179 $circulator->circ_permit_patron($scripts{circ_permit_patron});
180 $circulator->circ_permit_copy($scripts{circ_permit_copy});
181 $circulator->circ_duration($scripts{circ_duration});
182 $circulator->circ_permit_renew($scripts{circ_permit_renew});
184 $circulator->mk_env();
186 return circ_events($circulator) if $circulator->bail_out;
189 $circulator->override(1) if $api =~ /override/o;
191 if( $api =~ /checkout\.permit/ ) {
192 $circulator->do_permit();
194 } elsif( $api =~ /checkout.full/ ) {
196 # requesting a precat checkout implies that any required
197 # overrides have been performed. Go ahead and re-override.
198 $circulator->override(1) if $circulator->request_precat;
199 $circulator->do_permit();
200 $circulator->is_checkout(1);
201 unless( $circulator->bail_out ) {
202 $circulator->events([]);
203 $circulator->do_checkout();
206 } elsif( $api =~ /inspect/ ) {
207 my $data = $circulator->do_inspect();
208 $circulator->editor->rollback;
211 } elsif( $api =~ /checkout/ ) {
212 $circulator->is_checkout(1);
213 $circulator->do_checkout();
215 } elsif( $api =~ /checkin/ ) {
216 $circulator->do_checkin();
218 } elsif( $api =~ /renew/ ) {
219 $circulator->is_renewal(1);
220 $circulator->do_renew();
223 if( $circulator->bail_out ) {
226 # make sure no success event accidentally slip in
228 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
231 my @e = @{$circulator->events};
232 push( @ee, $_->{textcode} ) for @e;
233 $logger->info("circulator: bailing out with events: @ee");
235 $circulator->editor->rollback;
238 $circulator->editor->commit;
241 $circulator->script_runner->cleanup if $circulator->script_runner;
243 $conn->respond_complete(circ_events($circulator));
245 unless($circulator->bail_out) {
246 $circulator->do_hold_notify($circulator->notify_hold)
247 if $circulator->notify_hold;
248 $circulator->retarget_holds if $circulator->retarget;
249 $circulator->append_reading_list;
250 $circulator->make_trigger_events;
256 my @e = @{$circ->events};
257 # if we have multiple events, SUCCESS should not be one of them;
258 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
259 return (@e == 1) ? $e[0] : \@e;
263 sub translate_legacy_args {
266 if( $$args{barcode} ) {
267 $$args{copy_barcode} = $$args{barcode};
268 delete $$args{barcode};
271 if( $$args{copyid} ) {
272 $$args{copy_id} = $$args{copyid};
273 delete $$args{copyid};
276 if( $$args{patronid} ) {
277 $$args{patron_id} = $$args{patronid};
278 delete $$args{patronid};
281 if( $$args{patron} and !ref($$args{patron}) ) {
282 $$args{patron_id} = $$args{patron};
283 delete $$args{patron};
287 if( $$args{noncat} ) {
288 $$args{is_noncat} = $$args{noncat};
289 delete $$args{noncat};
292 if( $$args{precat} ) {
293 $$args{is_precat} = $$args{request_precat} = $$args{precat};
294 delete $$args{precat};
300 # --------------------------------------------------------------------------
301 # This package actually manages all of the circulation logic
302 # --------------------------------------------------------------------------
303 package OpenILS::Application::Circ::Circulator;
304 use strict; use warnings;
305 use vars q/$AUTOLOAD/;
307 use OpenILS::Utils::Fieldmapper;
308 use OpenSRF::Utils::Cache;
309 use Digest::MD5 qw(md5_hex);
310 use DateTime::Format::ISO8601;
311 use OpenILS::Utils::PermitHold;
312 use OpenSRF::Utils qw/:datetime/;
313 use OpenSRF::Utils::SettingsClient;
314 use OpenILS::Application::Circ::Holds;
315 use OpenILS::Application::Circ::Transit;
316 use OpenSRF::Utils::Logger qw(:logger);
317 use OpenILS::Utils::CStoreEditor qw/:funcs/;
318 use OpenILS::Application::Circ::ScriptBuilder;
319 use OpenILS::Const qw/:const/;
320 use OpenILS::Utils::Penalty;
321 use OpenILS::Application::Circ::CircCommon;
324 my $holdcode = "OpenILS::Application::Circ::Holds";
325 my $transcode = "OpenILS::Application::Circ::Transit";
331 # --------------------------------------------------------------------------
332 # Add a pile of automagic getter/setter methods
333 # --------------------------------------------------------------------------
334 my @AUTOLOAD_FIELDS = qw/
378 recurring_fines_level
391 cancelled_hold_transit
397 circ_matrix_matchpoint
399 legacy_script_support
411 my $type = ref($self) or die "$self is not an object";
413 my $name = $AUTOLOAD;
416 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
417 $logger->error("circulator: $type: invalid autoload field: $name");
418 die "$type: invalid autoload field: $name\n"
423 *{"${type}::${name}"} = sub {
426 $s->{$name} = $v if defined $v;
430 return $self->$name($data);
435 my( $class, $auth, %args ) = @_;
436 $class = ref($class) || $class;
437 my $self = bless( {}, $class );
440 $self->editor(new_editor(xact => 1, authtoken => $auth));
442 unless( $self->editor->checkauth ) {
443 $self->bail_on_events($self->editor->event);
447 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
449 $self->$_($args{$_}) for keys %args;
452 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
454 # if this is a renewal, default to desk_renewal
455 $self->desk_renewal(1) unless
456 $self->opac_renewal or $self->phone_renewal;
458 $self->capture('') unless $self->capture;
460 unless(%user_groups) {
461 my $gps = $self->editor->retrieve_all_permission_grp_tree;
462 %user_groups = map { $_->id => $_ } @$gps;
469 # --------------------------------------------------------------------------
470 # True if we should discontinue processing
471 # --------------------------------------------------------------------------
473 my( $self, $bool ) = @_;
474 if( defined $bool ) {
475 $logger->info("circulator: BAILING OUT") if $bool;
476 $self->{bail_out} = $bool;
478 return $self->{bail_out};
483 my( $self, @evts ) = @_;
486 $logger->info("circulator: pushing event ".$e->{textcode});
487 push( @{$self->events}, $e ) unless
488 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
494 my $key = md5_hex( time() . rand() . "$$" );
495 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
496 return $self->permit_key($key);
499 sub check_permit_key {
501 my $key = $self->permit_key;
502 return 0 unless $key;
503 my $k = "oils_permit_key_$key";
504 my $one = $self->cache_handle->get_cache($k);
505 $self->cache_handle->delete_cache($k);
506 return ($one) ? 1 : 0;
511 my $e = $self->editor;
513 # --------------------------------------------------------------------------
514 # Grab the fleshed copy
515 # --------------------------------------------------------------------------
516 unless($self->is_noncat) {
520 flesh_fields => {acp => ['call_number'], acn => ['record']}
523 $copy = $e->retrieve_asset_copy(
524 [$self->copy_id, $flesh ]) or return $e->event;
526 } elsif( $self->copy_barcode ) {
528 $copy = $e->search_asset_copy(
529 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
534 $self->volume($copy->call_number);
535 $self->title($self->volume->record);
536 $self->copy->call_number($self->volume->id);
537 $self->volume->record($self->title->id);
538 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
539 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
540 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
541 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
544 # We can't renew if there is no copy
545 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
546 if $self->is_renewal;
551 # --------------------------------------------------------------------------
553 # --------------------------------------------------------------------------
555 if( $self->patron_id ) {
556 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
558 } elsif( $self->patron_barcode ) {
560 my $card = $e->search_actor_card(
561 {barcode => $self->patron_barcode})->[0] or return $e->event;
563 $patron = $e->search_actor_user(
564 {card => $card->id})->[0] or return $e->event;
567 if( my $copy = $self->copy ) {
568 my $circs = $e->search_action_circulation(
569 {target_copy => $copy->id, checkin_time => undef});
571 if( my $circ = $circs->[0] ) {
572 $patron = $e->retrieve_actor_user($circ->usr)
578 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
579 unless $self->patron($patron) or $self->is_checkin;
582 # --------------------------------------------------------------------------
583 # This builds the script runner environment and fetches most of the
585 # --------------------------------------------------------------------------
586 sub mk_script_runner {
592 qw/copy copy_barcode copy_id patron
593 patron_id patron_barcode volume title editor/;
595 # Translate our objects into the ScriptBuilder args hash
596 $$args{$_} = $self->$_() for @fields;
598 $args->{ignore_user_status} = 1 if $self->is_checkin;
599 $$args{fetch_patron_by_circ_copy} = 1;
600 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
602 if( my $pco = $self->pending_checkouts ) {
603 $logger->info("circulator: we were given a pending checkouts number of $pco");
604 $$args{patronItemsOut} = $pco;
607 # This fetches most of the objects we need
608 $self->script_runner(
609 OpenILS::Application::Circ::ScriptBuilder->build($args));
611 # Now we translate the ScriptBuilder objects back into self
612 $self->$_($$args{$_}) for @fields;
614 my @evts = @{$args->{_events}} if $args->{_events};
616 $logger->debug("circulator: script builder returned events: @evts") if @evts;
620 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
621 if(!$self->is_noncat and
623 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
627 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
628 return $self->bail_on_events(@e);
633 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
634 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
635 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
636 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
640 # We can't renew if there is no copy
641 return $self->bail_on_events(@evts) if
642 $self->is_renewal and !$self->copy;
644 # Set some circ-specific flags in the script environment
645 my $evt = "environment";
646 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
648 if( $self->is_noncat ) {
649 $self->script_runner->insert("$evt.isNonCat", 1);
650 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
653 if( $self->is_precat ) {
654 $self->script_runner->insert("environment.isPrecat", 1, 1);
657 $self->script_runner->add_path( $_ ) for @$script_libs;
662 # --------------------------------------------------------------------------
663 # Does the circ permit work
664 # --------------------------------------------------------------------------
668 $self->log_me("do_permit()");
670 unless( $self->editor->requestor->id == $self->patron->id ) {
671 return $self->bail_on_events($self->editor->event)
672 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
675 $self->check_captured_holds();
676 $self->do_copy_checks();
677 return if $self->bail_out;
678 $self->run_patron_permit_scripts();
679 $self->run_copy_permit_scripts()
680 unless $self->is_precat or $self->is_noncat;
681 $self->check_item_deposit_events();
682 $self->override_events();
683 return if $self->bail_out;
685 if($self->is_precat and not $self->request_precat) {
688 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
689 return $self->bail_out(1) unless $self->is_renewal;
693 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
696 sub check_item_deposit_events {
698 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
699 if $self->is_deposit and not $self->is_deposit_exempt;
700 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
701 if $self->is_rental and not $self->is_rental_exempt;
704 # returns true if the user is not required to pay deposits
705 sub is_deposit_exempt {
707 my $pid = (ref $self->patron->profile) ?
708 $self->patron->profile->id : $self->patron->profile;
709 my $groups = $U->ou_ancestor_setting_value(
710 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
711 for my $grp (@$groups) {
712 return 1 if $self->is_group_descendant($grp, $pid);
717 # returns true if the user is not required to pay rental fees
718 sub is_rental_exempt {
720 my $pid = (ref $self->patron->profile) ?
721 $self->patron->profile->id : $self->patron->profile;
722 my $groups = $U->ou_ancestor_setting_value(
723 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
724 for my $grp (@$groups) {
725 return 1 if $self->is_group_descendant($grp, $pid);
730 sub is_group_descendant {
731 my($self, $p_id, $c_id) = @_;
732 return 0 unless defined $p_id and defined $c_id;
733 return 1 if $c_id == $p_id;
734 while(my $grp = $user_groups{$c_id}) {
735 $c_id = $grp->parent;
736 return 0 unless defined $c_id;
737 return 1 if $c_id == $p_id;
742 sub check_captured_holds {
744 my $copy = $self->copy;
745 my $patron = $self->patron;
747 return undef unless $copy;
749 my $s = $U->copy_status($copy->status)->id;
750 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
751 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
753 # Item is on the holds shelf, make sure it's going to the right person
754 my $holds = $self->editor->search_action_hold_request(
757 current_copy => $copy->id ,
758 capture_time => { '!=' => undef },
759 cancel_time => undef,
760 fulfillment_time => undef
766 if( $holds and $$holds[0] ) {
767 return undef if $$holds[0]->usr == $patron->id;
770 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
772 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
778 my $copy = $self->copy;
781 my $stat = $U->copy_status($copy->status)->id;
783 # We cannot check out a copy if it is in-transit
784 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
785 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
788 $self->handle_claims_returned();
789 return if $self->bail_out;
791 # no claims returned circ was found, check if there is any open circ
792 unless( $self->is_renewal ) {
794 my $circs = $self->editor->search_action_circulation(
795 { target_copy => $copy->id, checkin_time => undef }
798 if(my $old_circ = $circs->[0]) { # an open circ was found
800 my $payload; # event payload
802 if($old_circ->usr == $self->patron->id) {
804 $payload = {old_circ => $old_circ};
806 # If there is an open circulation on the checkout item and an auto-renew
807 # interval is defined, inform the caller that they should go
808 # ahead and renew the item instead of warning about open circulations.
810 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
811 $self->editor->requestor->ws_ou,
812 'circ.checkout_auto_renew_age',
816 if($auto_renew_intvl) {
817 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
818 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($old_circ->xact_start) );
820 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
821 $payload->{auto_renew} = 1;
826 return $self->bail_on_events(
827 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
833 my $LEGACY_CIRC_EVENT_MAP = {
834 'actor.usr.barred' => 'PATRON_BARRED',
835 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
836 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
837 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
838 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
839 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
840 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
841 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
842 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
846 # ---------------------------------------------------------------------
847 # This pushes any patron-related events into the list but does not
848 # set bail_out for any events
849 # ---------------------------------------------------------------------
850 sub run_patron_permit_scripts {
852 my $runner = $self->script_runner;
853 my $patronid = $self->patron->id;
857 if(!$self->legacy_script_support) {
859 my $results = $self->run_indb_circ_test;
860 unless($self->circ_test_success) {
861 push(@allevents, OpenILS::Event->new(
862 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
868 # ---------------------------------------------------------------------
869 # # Now run the patron permit script
870 # ---------------------------------------------------------------------
871 $runner->load($self->circ_permit_patron);
872 my $result = $runner->run or
873 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
875 my $patron_events = $result->{events};
877 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
878 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
879 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
880 $penalties = $penalties->{fatal_penalties};
882 for my $pen (@$penalties) {
883 my $event = OpenILS::Event->new($pen->name);
884 $event->{desc} = $pen->label;
885 push(@allevents, $event);
888 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
891 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
893 $self->push_events(@allevents);
896 sub run_indb_circ_test {
898 return $self->matrix_test_result if $self->matrix_test_result;
900 my $dbfunc = ($self->is_renewal) ?
901 'action.item_user_renew_test' : 'action.item_user_circ_test';
903 my $results = $self->editor->json_query(
906 $self->editor->requestor->ws_ou,
907 ($self->is_precat or $self->is_noncat) ? undef : $self->copy->id,
913 $self->circ_test_success($U->is_true($results->[0]->{success}));
915 if(my $mp = $results->[0]->{matchpoint}) {
916 $self->circ_matrix_matchpoint(
917 $self->editor->retrieve_config_circ_matrix_matchpoint([
920 flesh_fields => {ccmm =>
921 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
927 return $self->matrix_test_result($results);
930 # ---------------------------------------------------------------------
931 # given a use and copy, this will calculate the circulation policy
932 # parameters. Only works with in-db circ.
933 # ---------------------------------------------------------------------
937 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
939 $self->run_indb_circ_test;
942 circ_test_success => $self->circ_test_success,
943 failure_events => [],
947 unless($self->circ_test_success) {
948 push(@{$results->{failure_codes}},
949 $_->{fail_part}) for @{$self->matrix_test_result};
950 push(@{$results->{failure_events}},
951 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part})
952 for @{$self->matrix_test_result};
955 if($self->circ_matrix_matchpoint) {
956 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
957 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
958 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
960 my $policy = $self->get_circ_policy(
961 $duration_rule, $recurring_fine_rule, $max_fine_rule);
963 $$results{$_} = $$policy{$_} for keys %$policy;
969 # ---------------------------------------------------------------------
970 # Loads the circ policy info for duration, recurring fine, and max
971 # fine based on the current copy
972 # ---------------------------------------------------------------------
973 sub get_circ_policy {
974 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
977 duration_rule => $duration_rule->name,
978 recurring_fine_rule => $recurring_fine_rule->name,
979 max_fine_rule => $max_fine_rule->name,
980 max_fine => $self->get_max_fine_amount($max_fine_rule),
981 fine_interval => $recurring_fine_rule->recurance_interval,
982 renewal_remaining => $duration_rule->max_renewals
985 $policy->{duration} = $duration_rule->shrt
986 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
987 $policy->{duration} = $duration_rule->normal
988 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
989 $policy->{duration} = $duration_rule->extended
990 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
992 $policy->{recurring_fine} = $recurring_fine_rule->low
993 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
994 $policy->{recurring_fine} = $recurring_fine_rule->normal
995 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
996 $policy->{recurring_fine} = $recurring_fine_rule->high
997 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1002 sub get_max_fine_amount {
1004 my $max_fine_rule = shift;
1005 my $max_amount = $max_fine_rule->amount;
1007 # if is_percent is true then the max->amount is
1008 # use as a percentage of the copy price
1009 if ($U->is_true($max_fine_rule->is_percent)) {
1010 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1011 $max_amount = $price * $max_fine_rule->amount / 100;
1019 sub run_copy_permit_scripts {
1021 my $copy = $self->copy || return;
1022 my $runner = $self->script_runner;
1026 if(!$self->legacy_script_support) {
1027 my $results = $self->run_indb_circ_test;
1028 unless($self->circ_test_success) {
1029 push(@allevents, OpenILS::Event->new(
1030 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
1035 # ---------------------------------------------------------------------
1036 # Capture all of the copy permit events
1037 # ---------------------------------------------------------------------
1038 $runner->load($self->circ_permit_copy);
1039 my $result = $runner->run or
1040 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1041 my $copy_events = $result->{events};
1043 # ---------------------------------------------------------------------
1044 # Now collect all of the events together
1045 # ---------------------------------------------------------------------
1046 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1049 # See if this copy has an alert message
1050 my $ae = $self->check_copy_alert();
1051 push( @allevents, $ae ) if $ae;
1053 # uniquify the events
1054 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1055 @allevents = values %hash;
1058 $_->{payload} = $copy if
1059 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1062 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1064 $self->push_events(@allevents);
1068 sub check_copy_alert {
1070 return undef if $self->is_renewal;
1071 return OpenILS::Event->new(
1072 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1073 if $self->copy and $self->copy->alert_message;
1079 # --------------------------------------------------------------------------
1080 # If the call is overriding and has permissions to override every collected
1081 # event, the are cleared. Any event that the caller does not have
1082 # permission to override, will be left in the event list and bail_out will
1084 # XXX We need code in here to cancel any holds/transits on copies
1085 # that are being force-checked out
1086 # --------------------------------------------------------------------------
1087 sub override_events {
1089 my @events = @{$self->events};
1090 return unless @events;
1092 if(!$self->override) {
1093 return $self->bail_out(1)
1094 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1099 for my $e (@events) {
1100 my $tc = $e->{textcode};
1101 next if $tc eq 'SUCCESS';
1102 my $ov = "$tc.override";
1103 $logger->info("circulator: attempting to override event: $ov");
1105 return $self->bail_on_events($self->editor->event)
1106 unless( $self->editor->allowed($ov) );
1111 # --------------------------------------------------------------------------
1112 # If there is an open claimsreturn circ on the requested copy, close the
1113 # circ if overriding, otherwise bail out
1114 # --------------------------------------------------------------------------
1115 sub handle_claims_returned {
1117 my $copy = $self->copy;
1119 my $CR = $self->editor->search_action_circulation(
1121 target_copy => $copy->id,
1122 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1123 checkin_time => undef,
1127 return unless ($CR = $CR->[0]);
1131 # - If the caller has set the override flag, we will check the item in
1132 if($self->override) {
1134 $CR->checkin_time('now');
1135 $CR->checkin_scan_time('now');
1136 $CR->checkin_lib($self->editor->requestor->ws_ou);
1137 $CR->checkin_workstation($self->editor->requestor->wsid);
1138 $CR->checkin_staff($self->editor->requestor->id);
1140 $evt = $self->editor->event
1141 unless $self->editor->update_action_circulation($CR);
1144 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1147 $self->bail_on_events($evt) if $evt;
1152 # --------------------------------------------------------------------------
1153 # This performs the checkout
1154 # --------------------------------------------------------------------------
1158 $self->log_me("do_checkout()");
1160 # make sure perms are good if this isn't a renewal
1161 unless( $self->is_renewal ) {
1162 return $self->bail_on_events($self->editor->event)
1163 unless( $self->editor->allowed('COPY_CHECKOUT') );
1166 # verify the permit key
1167 unless( $self->check_permit_key ) {
1168 if( $self->permit_override ) {
1169 return $self->bail_on_events($self->editor->event)
1170 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1172 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1176 # if this is a non-cataloged circ, build the circ and finish
1177 if( $self->is_noncat ) {
1178 $self->checkout_noncat;
1180 OpenILS::Event->new('SUCCESS',
1181 payload => { noncat_circ => $self->circ }));
1185 if( $self->is_precat ) {
1186 $self->make_precat_copy;
1187 return if $self->bail_out;
1189 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1190 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1193 $self->do_copy_checks;
1194 return if $self->bail_out;
1196 $self->run_checkout_scripts();
1197 return if $self->bail_out;
1199 $self->build_checkout_circ_object();
1200 return if $self->bail_out;
1202 $self->apply_modified_due_date();
1203 return if $self->bail_out;
1205 return $self->bail_on_events($self->editor->event)
1206 unless $self->editor->create_action_circulation($self->circ);
1208 # refresh the circ to force local time zone for now
1209 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1211 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1213 return if $self->bail_out;
1215 $self->apply_deposit_fee();
1216 return if $self->bail_out;
1218 $self->handle_checkout_holds();
1219 return if $self->bail_out;
1221 # ------------------------------------------------------------------------------
1222 # Update the patron penalty info in the DB. Run it for permit-overrides
1223 # since the penalties are not updated during the permit phase
1224 # ------------------------------------------------------------------------------
1225 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1227 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1229 OpenILS::Event->new('SUCCESS',
1231 copy => $U->unflesh_copy($self->copy),
1232 circ => $self->circ,
1234 holds_fulfilled => $self->fulfilled_holds,
1235 deposit_billing => $self->deposit_billing,
1236 rental_billing => $self->rental_billing
1242 sub apply_deposit_fee {
1244 my $copy = $self->copy;
1246 ($self->is_deposit and not $self->is_deposit_exempt) or
1247 ($self->is_rental and not $self->is_rental_exempt);
1249 my $bill = Fieldmapper::money::billing->new;
1250 my $amount = $copy->deposit_amount;
1254 if($self->is_deposit) {
1255 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1257 $self->deposit_billing($bill);
1259 $billing_type = OILS_BILLING_TYPE_RENTAL;
1261 $self->rental_billing($bill);
1264 $bill->xact($self->circ->id);
1265 $bill->amount($amount);
1266 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1267 $bill->billing_type($billing_type);
1268 $bill->btype($btype);
1269 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1271 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1276 my $copy = $self->copy;
1278 my $stat = $copy->status if ref $copy->status;
1279 my $loc = $copy->location if ref $copy->location;
1280 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1282 $copy->status($stat->id) if $stat;
1283 $copy->location($loc->id) if $loc;
1284 $copy->circ_lib($circ_lib->id) if $circ_lib;
1285 $copy->editor($self->editor->requestor->id);
1286 $copy->edit_date('now');
1287 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1289 return $self->bail_on_events($self->editor->event)
1290 unless $self->editor->update_asset_copy($self->copy);
1292 $copy->status($U->copy_status($copy->status));
1293 $copy->location($loc) if $loc;
1294 $copy->circ_lib($circ_lib) if $circ_lib;
1298 sub bail_on_events {
1299 my( $self, @evts ) = @_;
1300 $self->push_events(@evts);
1305 # ------------------------------------------------------------------------------
1306 # When an item is checked out, see if we can fulfill a hold for this patron
1307 # ------------------------------------------------------------------------------
1308 sub handle_checkout_holds {
1310 my $copy = $self->copy;
1311 my $patron = $self->patron;
1313 my $e = $self->editor;
1314 $self->fulfilled_holds([]);
1316 # pre/non-cats can't fulfill a hold
1317 return if $self->is_precat or $self->is_noncat;
1319 my $hold = $e->search_action_hold_request({
1320 current_copy => $copy->id ,
1321 cancel_time => undef,
1322 fulfillment_time => undef,
1324 {expire_time => undef},
1325 {expire_time => {'>' => 'now'}}
1329 if($hold and $hold->usr != $patron->id) {
1330 # reset the hold since the copy is now checked out
1332 $logger->info("circulator: un-targeting hold ".$hold->id.
1333 " because copy ".$copy->id." is getting checked out");
1335 $hold->clear_prev_check_time;
1336 $hold->clear_current_copy;
1337 $hold->clear_capture_time;
1339 return $self->bail_on_event($e->event)
1340 unless $e->update_action_hold_request($hold);
1346 $hold = $self->find_related_user_hold($copy, $patron) or return;
1347 $logger->info("circulator: found related hold to fulfill in checkout");
1350 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1352 # if the hold was never officially captured, capture it.
1353 $hold->current_copy($copy->id);
1354 $hold->capture_time('now') unless $hold->capture_time;
1355 $hold->fulfillment_time('now');
1356 $hold->fulfillment_staff($e->requestor->id);
1357 $hold->fulfillment_lib($e->requestor->ws_ou);
1359 return $self->bail_on_events($e->event)
1360 unless $e->update_action_hold_request($hold);
1362 $holdcode->delete_hold_copy_maps($e, $hold->id);
1363 return $self->fulfilled_holds([$hold->id]);
1367 # ------------------------------------------------------------------------------
1368 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1369 # the patron directly targets the checked out item, see if there is another hold
1370 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1371 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1372 # ------------------------------------------------------------------------------
1373 sub find_related_user_hold {
1374 my($self, $copy, $patron) = @_;
1375 my $e = $self->editor;
1377 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1379 return undef unless $U->ou_ancestor_setting_value(
1380 $e->requestor->ws_ou, 'circ.checkout_fills_related_hold', $e);
1382 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1384 select => {ahr => ['id']},
1389 fkey => 'current_copy',
1390 type => 'left' # there may be no current_copy
1397 fulfillment_time => undef,
1398 cancel_time => undef,
1400 {expire_time => undef},
1401 {expire_time => {'>' => 'now'}}
1408 target => $self->volume->id
1414 target => $self->title->id
1420 {id => undef}, # left-join copy may be nonexistent
1421 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1425 order_by => {ahr => {request_time => {direction => 'asc'}}},
1429 my $hold_info = $e->json_query($args)->[0];
1430 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1435 sub run_checkout_scripts {
1439 my $runner = $self->script_runner;
1448 if(!$self->legacy_script_support) {
1449 $self->run_indb_circ_test();
1450 $duration = $self->circ_matrix_matchpoint->duration_rule;
1451 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1452 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1456 $runner->load($self->circ_duration);
1458 my $result = $runner->run or
1459 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1461 $duration_name = $result->{durationRule};
1462 $recurring_name = $result->{recurringFinesRule};
1463 $max_fine_name = $result->{maxFine};
1466 $duration_name = $duration->name if $duration;
1467 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1470 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1471 return $self->bail_on_events($evt) if $evt;
1473 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1474 return $self->bail_on_events($evt) if $evt;
1476 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1477 return $self->bail_on_events($evt) if $evt;
1482 # The item circulates with an unlimited duration
1488 $self->duration_rule($duration);
1489 $self->recurring_fines_rule($recurring);
1490 $self->max_fine_rule($max_fine);
1494 sub build_checkout_circ_object {
1497 my $circ = Fieldmapper::action::circulation->new;
1498 my $duration = $self->duration_rule;
1499 my $max = $self->max_fine_rule;
1500 my $recurring = $self->recurring_fines_rule;
1501 my $copy = $self->copy;
1502 my $patron = $self->patron;
1506 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1508 my $dname = $duration->name;
1509 my $mname = $max->name;
1510 my $rname = $recurring->name;
1512 $logger->debug("circulator: building circulation ".
1513 "with duration=$dname, maxfine=$mname, recurring=$rname");
1515 $circ->duration($policy->{duration});
1516 $circ->recuring_fine($policy->{recurring_fine});
1517 $circ->duration_rule($duration->name);
1518 $circ->recuring_fine_rule($recurring->name);
1519 $circ->max_fine_rule($max->name);
1520 $circ->max_fine($policy->{max_fine});
1521 $circ->fine_interval($recurring->recurance_interval);
1522 $circ->renewal_remaining($duration->max_renewals);
1526 $logger->info("circulator: copy found with an unlimited circ duration");
1527 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1528 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1529 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1530 $circ->renewal_remaining(0);
1533 $circ->target_copy( $copy->id );
1534 $circ->usr( $patron->id );
1535 $circ->circ_lib( $self->circ_lib );
1536 $circ->workstation($self->editor->requestor->wsid)
1537 if defined $self->editor->requestor->wsid;
1539 if( $self->is_renewal ) {
1540 $circ->opac_renewal('t') if $self->opac_renewal;
1541 $circ->phone_renewal('t') if $self->phone_renewal;
1542 $circ->desk_renewal('t') if $self->desk_renewal;
1543 $circ->renewal_remaining($self->renewal_remaining);
1544 $circ->circ_staff($self->editor->requestor->id);
1548 # if the user provided an overiding checkout time,
1549 # (e.g. the checkout really happened several hours ago), then
1550 # we apply that here. Does this need a perm??
1551 $circ->xact_start(clense_ISO8601($self->checkout_time))
1552 if $self->checkout_time;
1554 # if a patron is renewing, 'requestor' will be the patron
1555 $circ->circ_staff($self->editor->requestor->id);
1556 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1562 sub apply_modified_due_date {
1564 my $circ = $self->circ;
1565 my $copy = $self->copy;
1567 if( $self->due_date ) {
1569 return $self->bail_on_events($self->editor->event)
1570 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1572 $circ->due_date(clense_ISO8601($self->due_date));
1576 # if the due_date lands on a day when the location is closed
1577 return unless $copy and $circ->due_date;
1579 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1581 # due-date overlap should be determined by the location the item
1582 # is checked out from, not the owning or circ lib of the item
1583 my $org = $self->editor->requestor->ws_ou;
1585 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1586 " with an item due date of ".$circ->due_date );
1588 my $dateinfo = $U->storagereq(
1589 'open-ils.storage.actor.org_unit.closed_date.overlap',
1590 $org, $circ->due_date );
1593 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1594 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1596 # XXX make the behavior more dynamic
1597 # for now, we just push the due date to after the close date
1598 $circ->due_date($dateinfo->{end});
1605 sub create_due_date {
1606 my( $self, $duration ) = @_;
1608 # if there is a raw time component (e.g. from postgres),
1609 # turn it into an interval that interval_to_seconds can parse
1610 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1612 # for now, use the server timezone. TODO: use workstation org timezone
1613 my $due_date = DateTime->now(time_zone => 'local');
1615 # add the circ duration
1616 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
1618 # return ISO8601 time with timezone
1619 return $due_date->strftime('%FT%T%z');
1624 sub make_precat_copy {
1626 my $copy = $self->copy;
1629 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1631 $copy->editor($self->editor->requestor->id);
1632 $copy->edit_date('now');
1633 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
1634 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
1635 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
1636 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
1637 $self->update_copy();
1641 $logger->info("circulator: Creating a new precataloged ".
1642 "copy in checkout with barcode " . $self->copy_barcode);
1644 $copy = Fieldmapper::asset::copy->new;
1645 $copy->circ_lib($self->circ_lib);
1646 $copy->creator($self->editor->requestor->id);
1647 $copy->editor($self->editor->requestor->id);
1648 $copy->barcode($self->copy_barcode);
1649 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1650 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1651 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1653 $copy->dummy_title($self->dummy_title || "");
1654 $copy->dummy_author($self->dummy_author || "");
1655 $copy->dummy_isbn($self->dummy_isbn || "");
1656 $copy->circ_modifier($self->circ_modifier);
1659 # See if we need to override the circ_lib for the copy with a configured circ_lib
1660 # Setting is shortname of the org unit
1661 my $precat_circ_lib = $U->ou_ancestor_setting_value(
1662 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
1664 if($precat_circ_lib) {
1665 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
1668 $self->bail_on_events($self->editor->event);
1672 $copy->circ_lib($org->id);
1676 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1678 $self->push_events($self->editor->event);
1682 # this is a little bit of a hack, but we need to
1683 # get the copy into the script runner
1684 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1688 sub checkout_noncat {
1694 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1695 my $count = $self->noncat_count || 1;
1696 my $cotime = clense_ISO8601($self->checkout_time) || "";
1698 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
1702 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1703 $self->editor->requestor->id,
1711 $self->push_events($evt);
1722 $self->log_me("do_checkin()");
1724 return $self->bail_on_events(
1725 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1728 if( $self->checkin_check_holds_shelf() ) {
1729 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1730 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1731 $self->checkin_flesh_events;
1735 unless( $self->is_renewal ) {
1736 return $self->bail_on_events($self->editor->event)
1737 unless $self->editor->allowed('COPY_CHECKIN');
1740 $self->push_events($self->check_copy_alert());
1741 $self->push_events($self->check_checkin_copy_status());
1743 # the renew code will have already found our circulation object
1744 unless( $self->is_renewal and $self->circ ) {
1745 my $circs = $self->editor->search_action_circulation(
1746 { target_copy => $self->copy->id, checkin_time => undef });
1747 $self->circ($$circs[0]);
1749 # for now, just warn if there are multiple open circs on a copy
1750 $logger->warn("circulator: we have ".scalar(@$circs).
1751 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1754 # run the fine generator against this circ, if this circ is there
1755 $self->generate_fines if ($self->circ);
1757 # if the circ is marked as 'claims returned', add the event to the list
1758 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1759 if ($self->circ and $self->circ->stop_fines
1760 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1762 $self->check_circ_deposit();
1764 # handle the overridable events
1765 $self->override_events unless $self->is_renewal;
1766 return if $self->bail_out;
1770 $self->editor->search_action_transit_copy(
1771 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1775 $self->checkin_handle_circ;
1776 return if $self->bail_out;
1777 $self->checkin_changed(1);
1779 } elsif( $self->transit ) {
1780 my $hold_transit = $self->process_received_transit;
1781 $self->checkin_changed(1);
1783 if( $self->bail_out ) {
1784 $self->checkin_flesh_events;
1788 if( my $e = $self->check_checkin_copy_status() ) {
1789 # If the original copy status is special, alert the caller
1790 my $ev = $self->events;
1791 $self->events([$e]);
1792 $self->override_events;
1793 return if $self->bail_out;
1797 if( $hold_transit or
1798 $U->copy_status($self->copy->status)->id
1799 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1802 if( $hold_transit ) {
1803 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1805 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1810 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1812 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1813 $self->reshelve_copy(1);
1814 $self->cancelled_hold_transit(1);
1815 $self->notify_hold(0); # don't notify for cancelled holds
1816 return if $self->bail_out;
1820 # hold transited to correct location
1821 $self->checkin_flesh_events;
1826 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1828 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1829 " that is in-transit, but there is no transit.. repairing");
1830 $self->reshelve_copy(1);
1831 return if $self->bail_out;
1834 if( $self->is_renewal ) {
1835 $self->push_events(OpenILS::Event->new('SUCCESS'));
1839 # ------------------------------------------------------------------------------
1840 # Circulations and transits are now closed where necessary. Now go on to see if
1841 # this copy can fulfill a hold or needs to be routed to a different location
1842 # ------------------------------------------------------------------------------
1844 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1846 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1847 return if $self->bail_out;
1849 unless($needed_for_hold) {
1850 my $circ_lib = (ref $self->copy->circ_lib) ?
1851 $self->copy->circ_lib->id : $self->copy->circ_lib;
1853 if( $self->remote_hold ) {
1854 $circ_lib = $self->remote_hold->pickup_lib;
1855 $logger->warn("circulator: Copy ".$self->copy->barcode.
1856 " is on a remote hold's shelf, sending to $circ_lib");
1859 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1861 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1863 $self->checkin_handle_precat();
1864 return if $self->bail_out;
1868 my $bc = $self->copy->barcode;
1869 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1870 $self->checkin_build_copy_transit($circ_lib);
1871 return if $self->bail_out;
1872 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1877 $self->reshelve_copy;
1878 return if $self->bail_out;
1880 unless($self->checkin_changed) {
1882 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1883 my $stat = $U->copy_status($self->copy->status)->id;
1885 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1886 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1887 $self->bail_out(1); # no need to commit anything
1891 $self->push_events(OpenILS::Event->new('SUCCESS'))
1892 unless @{$self->events};
1895 OpenILS::Utils::Penalty->calculate_penalties(
1896 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1898 $self->checkin_flesh_events;
1902 # if a deposit was payed for this item, push the event
1903 sub check_circ_deposit {
1905 return unless $self->circ;
1906 my $deposit = $self->editor->search_money_billing(
1908 xact => $self->circ->id,
1910 }, {idlist => 1})->[0];
1912 $self->push_events(OpenILS::Event->new(
1913 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1918 my $force = $self->force || shift;
1919 my $copy = $self->copy;
1921 my $stat = $U->copy_status($copy->status)->id;
1924 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1925 $stat != OILS_COPY_STATUS_CATALOGING and
1926 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1927 $stat != OILS_COPY_STATUS_RESHELVING )) {
1929 $copy->status( OILS_COPY_STATUS_RESHELVING );
1931 $self->checkin_changed(1);
1936 # Returns true if the item is at the current location
1937 # because it was transited there for a hold and the
1938 # hold has not been fulfilled
1939 sub checkin_check_holds_shelf {
1941 return 0 unless $self->copy;
1944 $U->copy_status($self->copy->status)->id ==
1945 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1947 # find the hold that put us on the holds shelf
1948 my $holds = $self->editor->search_action_hold_request(
1950 current_copy => $self->copy->id,
1951 capture_time => { '!=' => undef },
1952 fulfillment_time => undef,
1953 cancel_time => undef,
1958 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1959 $self->reshelve_copy(1);
1963 my $hold = $$holds[0];
1965 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1966 $hold->id. "] for copy ".$self->copy->barcode);
1968 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1969 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1973 $logger->info("circulator: hold is not for here..");
1974 $self->remote_hold($hold);
1979 sub checkin_handle_precat {
1981 my $copy = $self->copy;
1983 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1984 $copy->status(OILS_COPY_STATUS_CATALOGING);
1985 $self->update_copy();
1986 $self->checkin_changed(1);
1987 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1992 sub checkin_build_copy_transit {
1995 my $copy = $self->copy;
1996 my $transit = Fieldmapper::action::transit_copy->new;
1998 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1999 $logger->info("circulator: transiting copy to $dest");
2001 $transit->source($self->editor->requestor->ws_ou);
2002 $transit->dest($dest);
2003 $transit->target_copy($copy->id);
2004 $transit->source_send_time('now');
2005 $transit->copy_status( $U->copy_status($copy->status)->id );
2007 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2009 return $self->bail_on_events($self->editor->event)
2010 unless $self->editor->create_action_transit_copy($transit);
2012 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2014 $self->checkin_changed(1);
2018 # returns true if the item was used (or may potentially be used
2019 # in subsequent calls) to capture a hold.
2020 sub attempt_checkin_hold_capture {
2022 my $copy = $self->copy;
2024 # we've been explicitly told not to capture any holds
2025 return 0 if $self->capture eq 'nocapture';
2027 # See if this copy can fulfill any holds
2028 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2029 $self->editor, $copy, $self->editor->requestor );
2032 $logger->debug("circulator: no potential permitted".
2033 "holds found for copy ".$copy->barcode);
2037 if($self->capture ne 'capture') {
2038 # see if this item is in a hold-capture-delay location
2039 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2040 if($U->is_true($location->hold_verify)) {
2041 $self->bail_on_events(
2042 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2047 $self->retarget($retarget);
2049 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2051 $hold->current_copy($copy->id);
2052 $hold->capture_time('now');
2053 $hold->shelf_time('now')
2054 if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2056 # prevent DB errors caused by fetching
2057 # holds from storage, and updating through cstore
2058 $hold->clear_fulfillment_time;
2059 $hold->clear_fulfillment_staff;
2060 $hold->clear_fulfillment_lib;
2061 $hold->clear_expire_time;
2062 $hold->clear_cancel_time;
2063 $hold->clear_prev_check_time unless $hold->prev_check_time;
2065 $self->bail_on_events($self->editor->event)
2066 unless $self->editor->update_action_hold_request($hold);
2068 $self->checkin_changed(1);
2070 return 0 if $self->bail_out;
2072 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2074 # This hold was captured in the correct location
2075 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2076 $self->push_events(OpenILS::Event->new('SUCCESS'));
2078 #$self->do_hold_notify($hold->id);
2079 $self->notify_hold($hold->id);
2083 # Hold needs to be picked up elsewhere. Build a hold
2084 # transit and route the item.
2085 $self->checkin_build_hold_transit();
2086 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2087 return 0 if $self->bail_out;
2088 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2091 # make sure we save the copy status
2096 sub do_hold_notify {
2097 my( $self, $holdid ) = @_;
2099 my $e = new_editor(xact => 1);
2100 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2102 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2103 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2105 $logger->info("circulator: running delayed hold notify process");
2107 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2108 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2110 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2111 hold_id => $holdid, requestor => $self->editor->requestor);
2113 $logger->debug("circulator: built hold notifier");
2115 if(!$notifier->event) {
2117 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2119 my $stat = $notifier->send_email_notify;
2120 if( $stat == '1' ) {
2121 $logger->info("circulator: hold notify succeeded for hold $holdid");
2125 $logger->warn("circulator: * hold notify failed for hold $holdid");
2128 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2132 sub retarget_holds {
2134 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2135 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2136 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2137 # no reason to wait for the return value
2141 sub checkin_build_hold_transit {
2144 my $copy = $self->copy;
2145 my $hold = $self->hold;
2146 my $trans = Fieldmapper::action::hold_transit_copy->new;
2148 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2150 $trans->hold($hold->id);
2151 $trans->source($self->editor->requestor->ws_ou);
2152 $trans->dest($hold->pickup_lib);
2153 $trans->source_send_time("now");
2154 $trans->target_copy($copy->id);
2156 # when the copy gets to its destination, it will recover
2157 # this status - put it onto the holds shelf
2158 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2160 return $self->bail_on_events($self->editor->event)
2161 unless $self->editor->create_action_hold_transit_copy($trans);
2166 sub process_received_transit {
2168 my $copy = $self->copy;
2169 my $copyid = $self->copy->id;
2171 my $status_name = $U->copy_status($copy->status)->name;
2172 $logger->debug("circulator: attempting transit receive on ".
2173 "copy $copyid. Copy status is $status_name");
2175 my $transit = $self->transit;
2177 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2178 # - this item is in-transit to a different location
2180 my $tid = $transit->id;
2181 my $loc = $self->editor->requestor->ws_ou;
2182 my $dest = $transit->dest;
2184 $logger->info("circulator: Fowarding transit on copy which is destined ".
2185 "for a different location. transit=$tid, copy=$copyid, current ".
2186 "location=$loc, destination location=$dest");
2188 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2190 # grab the associated hold object if available
2191 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2192 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2194 return $self->bail_on_events($evt);
2197 # The transit is received, set the receive time
2198 $transit->dest_recv_time('now');
2199 $self->bail_on_events($self->editor->event)
2200 unless $self->editor->update_action_transit_copy($transit);
2202 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2204 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2205 $copy->status( $transit->copy_status );
2206 $self->update_copy();
2207 return if $self->bail_out;
2211 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2213 # hold has arrived at destination, set shelf time
2214 $hold->shelf_time('now');
2215 $self->bail_on_events($self->editor->event)
2216 unless $self->editor->update_action_hold_request($hold);
2217 return if $self->bail_out;
2219 $self->notify_hold($hold_transit->hold);
2224 OpenILS::Event->new(
2227 payload => { transit => $transit, holdtransit => $hold_transit } ));
2229 return $hold_transit;
2233 sub generate_fines {
2238 my $st = OpenSRF::AppSession->connect('open-ils.storage');
2241 'open-ils.storage.action.circulation.overdue.generate_fines',
2248 # refresh the circ in case the fine generator set the stop_fines field
2249 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
2254 sub checkin_handle_circ {
2256 my $circ = $self->circ;
2257 my $copy = $self->copy;
2261 # backdate the circ if necessary
2262 if($self->backdate) {
2263 $self->checkin_handle_backdate;
2264 return if $self->bail_out;
2267 if(!$circ->stop_fines) {
2268 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2269 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2270 $circ->stop_fines_time('now') unless $self->backdate;
2271 $circ->stop_fines_time($self->backdate) if $self->backdate;
2274 # see if there are any fines owed on this circ. if not, close it
2275 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2276 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2278 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2280 # Set the checkin vars since we have the item
2281 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2283 # capture the true scan time for back-dated checkins
2284 $circ->checkin_scan_time('now');
2286 $circ->checkin_staff($self->editor->requestor->id);
2287 $circ->checkin_lib($self->editor->requestor->ws_ou);
2288 $circ->checkin_workstation($self->editor->requestor->wsid);
2290 my $circ_lib = (ref $self->copy->circ_lib) ?
2291 $self->copy->circ_lib->id : $self->copy->circ_lib;
2292 my $stat = $U->copy_status($self->copy->status)->id;
2294 # immediately available keeps items lost or missing items from going home before being handled
2295 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2296 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2299 if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2301 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2302 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2304 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2308 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2310 $self->checkin_handle_lost($circ_lib);
2314 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2318 return $self->bail_on_events($self->editor->event)
2319 unless $self->editor->update_action_circulation($circ);
2321 # make sure the circ isn't closed if we just voided some fines
2322 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2323 return $self->bail_on_events($evt) if $evt;
2329 # ------------------------------------------------------------------
2330 # See if we need to void billings for lost checkin
2331 # ------------------------------------------------------------------
2332 sub checkin_handle_lost {
2334 my $circ_lib = shift;
2335 my $circ = $self->circ;
2337 my $max_return = $U->ou_ancestor_setting_value(
2338 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2343 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2344 $tm[5] -= 1 if $tm[5] > 0;
2345 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2347 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2348 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2350 $max_return = 0 if $today < $last_chance;
2353 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2355 my $void_lost = $U->ou_ancestor_setting_value(
2356 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2357 my $void_lost_fee = $U->ou_ancestor_setting_value(
2358 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2359 my $restore_od = $U->ou_ancestor_setting_value(
2360 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2362 $self->checkin_handle_lost_now_found(3) if $void_lost;
2363 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2364 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2367 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2372 sub checkin_handle_backdate {
2375 my $bd = $self->backdate;
2377 # ------------------------------------------------------------------
2378 # clean up the backdate for date comparison
2379 # we want any bills created on or after the backdate
2380 # ------------------------------------------------------------------
2381 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2382 #$bd = "${bd}T23:59:59";
2384 my $bills = $self->editor->search_money_billing(
2386 billing_ts => { '>=' => $bd },
2387 xact => $self->circ->id,
2392 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2394 for my $bill (@$bills) {
2395 unless( $U->is_true($bill->voided) ) {
2396 $logger->info("backdate voiding bill ".$bill->id);
2398 $bill->void_time('now');
2399 $bill->voider($self->editor->requestor->id);
2400 my $n = $bill->note || "";
2401 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2403 $self->bail_on_events($self->editor->event)
2404 unless $self->editor->update_money_billing($bill);
2412 sub find_patron_from_copy {
2414 my $circs = $self->editor->search_action_circulation(
2415 { target_copy => $self->copy->id, checkin_time => undef });
2416 my $circ = $circs->[0];
2417 return unless $circ;
2418 my $u = $self->editor->retrieve_actor_user($circ->usr)
2419 or return $self->bail_on_events($self->editor->event);
2423 sub check_checkin_copy_status {
2425 my $copy = $self->copy;
2431 my $status = $U->copy_status($copy->status)->id;
2434 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2435 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2436 $status == OILS_COPY_STATUS_IN_PROCESS ||
2437 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2438 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2439 $status == OILS_COPY_STATUS_CATALOGING ||
2440 $status == OILS_COPY_STATUS_RESHELVING );
2442 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2443 if( $status == OILS_COPY_STATUS_LOST );
2445 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2446 if( $status == OILS_COPY_STATUS_MISSING );
2448 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2453 # --------------------------------------------------------------------------
2454 # On checkin, we need to return as many relevant objects as we can
2455 # --------------------------------------------------------------------------
2456 sub checkin_flesh_events {
2459 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2460 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2461 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2464 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2467 if($self->hold and !$self->hold->cancel_time) {
2468 $hold = $self->hold;
2469 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
2472 for my $evt (@{$self->events}) {
2475 $payload->{copy} = $U->unflesh_copy($self->copy);
2476 $payload->{record} = $record,
2477 $payload->{circ} = $self->circ;
2478 $payload->{transit} = $self->transit;
2479 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2480 $payload->{hold} = $hold;
2481 $evt->{payload} = $payload;
2486 my( $self, $msg ) = @_;
2487 my $bc = ($self->copy) ? $self->copy->barcode :
2490 my $usr = ($self->patron) ? $self->patron->id : "";
2491 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2492 ", recipient=$usr, copy=$bc");
2498 $self->log_me("do_renew()");
2500 # Make sure there is an open circ to renew that is not
2501 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2502 my $usrid = $self->patron->id if $self->patron;
2505 # If we have a patron, match them to the circ
2506 $circ = $self->editor->search_action_circulation(
2507 {target_copy => $self->copy->id, usr => $usrid, stop_fines => undef})->[0];
2509 $circ = $self->editor->search_action_circulation(
2510 {target_copy => $self->copy->id, stop_fines => undef})->[0];
2515 $circ = $self->editor->search_action_circulation(
2516 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2518 $circ = $self->editor->search_action_circulation(
2519 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2523 return $self->bail_on_events($self->editor->event) unless $circ;
2525 # A user is not allowed to renew another user's items without permission
2526 unless( $circ->usr eq $self->editor->requestor->id ) {
2527 return $self->bail_on_events($self->editor->events)
2528 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2531 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2532 if $circ->renewal_remaining < 1;
2534 # -----------------------------------------------------------------
2536 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2539 $self->run_renew_permit;
2542 $self->do_checkin();
2543 return if $self->bail_out;
2545 unless( $self->permit_override ) {
2547 return if $self->bail_out;
2548 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2549 $self->remove_event('ITEM_NOT_CATALOGED');
2552 $self->override_events;
2553 return if $self->bail_out;
2556 $self->do_checkout();
2561 my( $self, $evt ) = @_;
2562 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2563 $logger->debug("circulator: removing event from list: $evt");
2564 my @events = @{$self->events};
2565 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2570 my( $self, $evt ) = @_;
2571 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2572 return grep { $_->{textcode} eq $evt } @{$self->events};
2577 sub run_renew_permit {
2582 if(!$self->legacy_script_support) {
2583 my $results = $self->run_indb_circ_test;
2584 unless($self->circ_test_success) {
2585 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) for @$results;
2590 my $runner = $self->script_runner;
2592 $runner->load($self->circ_permit_renew);
2593 my $result = $runner->run or
2594 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2595 $events = $result->{events};
2596 $self->mk_script_runner;
2599 $logger->activity("circulator: circ_permit_renew for user ".
2600 $self->patron->id." returned events: @$events") if @$events;
2602 $self->push_events(OpenILS::Event->new($_)) for @$events;
2604 $logger->debug("circulator: re-creating script runner to be safe");
2608 sub append_reading_list {
2612 $self->is_checkout and
2617 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2619 # verify history is globally enabled and uses the bucket mechanism
2620 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2621 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2623 unless($htype eq 'bucket') {
2628 # verify the patron wants to retain the hisory
2629 my $setting = $e->search_actor_user_setting(
2630 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2632 unless($setting and $setting->value) {
2637 my $bkt = $e->search_container_copy_bucket(
2638 {owner => $self->patron->id, btype => 'circ_history'})->[0];
2643 # find the next item position
2644 my $last_item = $e->search_container_copy_bucket_item(
2645 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2646 $pos = $last_item->pos + 1 if $last_item;
2649 # create the history bucket if necessary
2650 $bkt = Fieldmapper::container::copy_bucket->new;
2651 $bkt->owner($self->patron->id);
2653 $bkt->btype('circ_history');
2655 $e->create_container_copy_bucket($bkt) or return $e->die_event;
2658 my $item = Fieldmapper::container::copy_bucket_item->new;
2660 $item->bucket($bkt->id);
2661 $item->target_copy($self->copy->id);
2664 $e->create_container_copy_bucket_item($item) or return $e->die_event;
2671 sub make_trigger_events {
2673 return unless $self->circ;
2674 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2675 $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2676 $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2677 $ses->request('open-ils.trigger.event.autocreate', 'renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
2684 sub checkin_handle_lost_now_found {
2685 my ($self, $bill_type) = @_;
2687 # ------------------------------------------------------------------
2688 # remove charge from patron's account if lost item is returned
2689 # ------------------------------------------------------------------
2691 my $bills = $self->editor->search_money_billing(
2693 xact => $self->circ->id,
2698 $logger->debug("voiding lost item charge of ".scalar(@$bills));
2699 for my $bill (@$bills) {
2700 if( !$U->is_true($bill->voided) ) {
2701 $logger->info("lost item returned - voiding bill ".$bill->id);
2703 $bill->void_time('now');
2704 $bill->voider($self->editor->requestor->id);
2705 my $note = ($bill->note) ? $bill->note . "\n" : '';
2706 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2708 $self->bail_on_events($self->editor->event)
2709 unless $self->editor->update_money_billing($bill);
2714 sub checkin_handle_lost_now_found_restore_od {
2717 # ------------------------------------------------------------------
2718 # restore those overdue charges voided when item was set to lost
2719 # ------------------------------------------------------------------
2721 my $ods = $self->editor->search_money_billing(
2723 xact => $self->circ->id,
2728 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
2729 for my $bill (@$ods) {
2730 if( $U->is_true($bill->voided) ) {
2731 $logger->info("lost item returned - restoring overdue ".$bill->id);
2733 $bill->clear_void_time;
2734 $bill->voider($self->editor->requestor->id);
2735 my $note = ($bill->note) ? $bill->note . "\n" : '';
2736 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2738 $self->bail_on_events($self->editor->event)
2739 unless $self->editor->update_money_billing($bill);