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/;
177 $circulator->noop if $circulator->claims_never_checked_out;
179 if($legacy_script_support and not $circulator->is_checkin) {
180 $circulator->mk_script_runner();
181 $circulator->legacy_script_support(1);
182 $circulator->circ_permit_patron($scripts{circ_permit_patron});
183 $circulator->circ_permit_copy($scripts{circ_permit_copy});
184 $circulator->circ_duration($scripts{circ_duration});
185 $circulator->circ_permit_renew($scripts{circ_permit_renew});
187 $circulator->mk_env();
189 return circ_events($circulator) if $circulator->bail_out;
192 $circulator->override(1) if $api =~ /override/o;
194 if( $api =~ /checkout\.permit/ ) {
195 $circulator->do_permit();
197 } elsif( $api =~ /checkout.full/ ) {
199 # requesting a precat checkout implies that any required
200 # overrides have been performed. Go ahead and re-override.
201 $circulator->override(1) if $circulator->request_precat;
202 $circulator->do_permit();
203 $circulator->is_checkout(1);
204 unless( $circulator->bail_out ) {
205 $circulator->events([]);
206 $circulator->do_checkout();
209 } elsif( $api =~ /inspect/ ) {
210 my $data = $circulator->do_inspect();
211 $circulator->editor->rollback;
214 } elsif( $api =~ /checkout/ ) {
215 $circulator->is_checkout(1);
216 $circulator->do_checkout();
218 } elsif( $api =~ /checkin/ ) {
219 $circulator->do_checkin();
221 } elsif( $api =~ /renew/ ) {
222 $circulator->is_renewal(1);
223 $circulator->do_renew();
226 if( $circulator->bail_out ) {
229 # make sure no success event accidentally slip in
231 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
234 my @e = @{$circulator->events};
235 push( @ee, $_->{textcode} ) for @e;
236 $logger->info("circulator: bailing out with events: @ee");
238 $circulator->editor->rollback;
241 $circulator->editor->commit;
244 $circulator->script_runner->cleanup if $circulator->script_runner;
246 $conn->respond_complete(circ_events($circulator));
248 unless($circulator->bail_out) {
249 $circulator->do_hold_notify($circulator->notify_hold)
250 if $circulator->notify_hold;
251 $circulator->retarget_holds if $circulator->retarget;
252 $circulator->append_reading_list;
253 $circulator->make_trigger_events;
259 my @e = @{$circ->events};
260 # if we have multiple events, SUCCESS should not be one of them;
261 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
262 return (@e == 1) ? $e[0] : \@e;
266 sub translate_legacy_args {
269 if( $$args{barcode} ) {
270 $$args{copy_barcode} = $$args{barcode};
271 delete $$args{barcode};
274 if( $$args{copyid} ) {
275 $$args{copy_id} = $$args{copyid};
276 delete $$args{copyid};
279 if( $$args{patronid} ) {
280 $$args{patron_id} = $$args{patronid};
281 delete $$args{patronid};
284 if( $$args{patron} and !ref($$args{patron}) ) {
285 $$args{patron_id} = $$args{patron};
286 delete $$args{patron};
290 if( $$args{noncat} ) {
291 $$args{is_noncat} = $$args{noncat};
292 delete $$args{noncat};
295 if( $$args{precat} ) {
296 $$args{is_precat} = $$args{request_precat} = $$args{precat};
297 delete $$args{precat};
303 # --------------------------------------------------------------------------
304 # This package actually manages all of the circulation logic
305 # --------------------------------------------------------------------------
306 package OpenILS::Application::Circ::Circulator;
307 use strict; use warnings;
308 use vars q/$AUTOLOAD/;
310 use OpenILS::Utils::Fieldmapper;
311 use OpenSRF::Utils::Cache;
312 use Digest::MD5 qw(md5_hex);
313 use DateTime::Format::ISO8601;
314 use OpenILS::Utils::PermitHold;
315 use OpenSRF::Utils qw/:datetime/;
316 use OpenSRF::Utils::SettingsClient;
317 use OpenILS::Application::Circ::Holds;
318 use OpenILS::Application::Circ::Transit;
319 use OpenSRF::Utils::Logger qw(:logger);
320 use OpenILS::Utils::CStoreEditor qw/:funcs/;
321 use OpenILS::Application::Circ::ScriptBuilder;
322 use OpenILS::Const qw/:const/;
323 use OpenILS::Utils::Penalty;
324 use OpenILS::Application::Circ::CircCommon;
327 my $holdcode = "OpenILS::Application::Circ::Holds";
328 my $transcode = "OpenILS::Application::Circ::Transit";
334 # --------------------------------------------------------------------------
335 # Add a pile of automagic getter/setter methods
336 # --------------------------------------------------------------------------
337 my @AUTOLOAD_FIELDS = qw/
381 recurring_fines_level
393 cancelled_hold_transit
399 circ_matrix_matchpoint
401 legacy_script_support
411 claims_never_checked_out
417 my $type = ref($self) or die "$self is not an object";
419 my $name = $AUTOLOAD;
422 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
423 $logger->error("circulator: $type: invalid autoload field: $name");
424 die "$type: invalid autoload field: $name\n"
429 *{"${type}::${name}"} = sub {
432 $s->{$name} = $v if defined $v;
436 return $self->$name($data);
441 my( $class, $auth, %args ) = @_;
442 $class = ref($class) || $class;
443 my $self = bless( {}, $class );
446 $self->editor(new_editor(xact => 1, authtoken => $auth));
448 unless( $self->editor->checkauth ) {
449 $self->bail_on_events($self->editor->event);
453 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
455 $self->$_($args{$_}) for keys %args;
458 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
460 # if this is a renewal, default to desk_renewal
461 $self->desk_renewal(1) unless
462 $self->opac_renewal or $self->phone_renewal;
464 $self->capture('') unless $self->capture;
466 unless(%user_groups) {
467 my $gps = $self->editor->retrieve_all_permission_grp_tree;
468 %user_groups = map { $_->id => $_ } @$gps;
475 # --------------------------------------------------------------------------
476 # True if we should discontinue processing
477 # --------------------------------------------------------------------------
479 my( $self, $bool ) = @_;
480 if( defined $bool ) {
481 $logger->info("circulator: BAILING OUT") if $bool;
482 $self->{bail_out} = $bool;
484 return $self->{bail_out};
489 my( $self, @evts ) = @_;
492 $logger->info("circulator: pushing event ".$e->{textcode});
493 push( @{$self->events}, $e ) unless
494 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
500 my $key = md5_hex( time() . rand() . "$$" );
501 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
502 return $self->permit_key($key);
505 sub check_permit_key {
507 my $key = $self->permit_key;
508 return 0 unless $key;
509 my $k = "oils_permit_key_$key";
510 my $one = $self->cache_handle->get_cache($k);
511 $self->cache_handle->delete_cache($k);
512 return ($one) ? 1 : 0;
517 my $e = $self->editor;
519 # --------------------------------------------------------------------------
520 # Grab the fleshed copy
521 # --------------------------------------------------------------------------
522 unless($self->is_noncat) {
526 flesh_fields => {acp => ['call_number'], acn => ['record']}
529 $copy = $e->retrieve_asset_copy(
530 [$self->copy_id, $flesh ]) or return $e->event;
532 } elsif( $self->copy_barcode ) {
534 $copy = $e->search_asset_copy(
535 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
540 $self->volume($copy->call_number);
541 $self->title($self->volume->record);
542 $self->copy->call_number($self->volume->id);
543 $self->volume->record($self->title->id);
544 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
545 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
546 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
547 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
550 # We can't renew if there is no copy
551 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
552 if $self->is_renewal;
557 # --------------------------------------------------------------------------
559 # --------------------------------------------------------------------------
561 if( $self->patron_id ) {
562 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
564 } elsif( $self->patron_barcode ) {
566 my $card = $e->search_actor_card(
567 {barcode => $self->patron_barcode})->[0] or return $e->event;
569 $patron = $e->search_actor_user(
570 {card => $card->id})->[0] or return $e->event;
573 if( my $copy = $self->copy ) {
574 my $circs = $e->search_action_circulation(
575 {target_copy => $copy->id, checkin_time => undef});
577 if( my $circ = $circs->[0] ) {
578 $patron = $e->retrieve_actor_user($circ->usr)
584 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
585 unless $self->patron($patron) or $self->is_checkin;
588 # --------------------------------------------------------------------------
589 # This builds the script runner environment and fetches most of the
591 # --------------------------------------------------------------------------
592 sub mk_script_runner {
598 qw/copy copy_barcode copy_id patron
599 patron_id patron_barcode volume title editor/;
601 # Translate our objects into the ScriptBuilder args hash
602 $$args{$_} = $self->$_() for @fields;
604 $args->{ignore_user_status} = 1 if $self->is_checkin;
605 $$args{fetch_patron_by_circ_copy} = 1;
606 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
608 if( my $pco = $self->pending_checkouts ) {
609 $logger->info("circulator: we were given a pending checkouts number of $pco");
610 $$args{patronItemsOut} = $pco;
613 # This fetches most of the objects we need
614 $self->script_runner(
615 OpenILS::Application::Circ::ScriptBuilder->build($args));
617 # Now we translate the ScriptBuilder objects back into self
618 $self->$_($$args{$_}) for @fields;
620 my @evts = @{$args->{_events}} if $args->{_events};
622 $logger->debug("circulator: script builder returned events: @evts") if @evts;
626 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
627 if(!$self->is_noncat and
629 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
633 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
634 return $self->bail_on_events(@e);
639 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
640 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
641 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
642 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
646 # We can't renew if there is no copy
647 return $self->bail_on_events(@evts) if
648 $self->is_renewal and !$self->copy;
650 # Set some circ-specific flags in the script environment
651 my $evt = "environment";
652 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
654 if( $self->is_noncat ) {
655 $self->script_runner->insert("$evt.isNonCat", 1);
656 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
659 if( $self->is_precat ) {
660 $self->script_runner->insert("environment.isPrecat", 1, 1);
663 $self->script_runner->add_path( $_ ) for @$script_libs;
668 # --------------------------------------------------------------------------
669 # Does the circ permit work
670 # --------------------------------------------------------------------------
674 $self->log_me("do_permit()");
676 unless( $self->editor->requestor->id == $self->patron->id ) {
677 return $self->bail_on_events($self->editor->event)
678 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
681 $self->check_captured_holds();
682 $self->do_copy_checks();
683 return if $self->bail_out;
684 $self->run_patron_permit_scripts();
685 $self->run_copy_permit_scripts()
686 unless $self->is_precat or $self->is_noncat;
687 $self->check_item_deposit_events();
688 $self->override_events();
689 return if $self->bail_out;
691 if($self->is_precat and not $self->request_precat) {
694 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
695 return $self->bail_out(1) unless $self->is_renewal;
699 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
702 sub check_item_deposit_events {
704 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
705 if $self->is_deposit and not $self->is_deposit_exempt;
706 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
707 if $self->is_rental and not $self->is_rental_exempt;
710 # returns true if the user is not required to pay deposits
711 sub is_deposit_exempt {
713 my $pid = (ref $self->patron->profile) ?
714 $self->patron->profile->id : $self->patron->profile;
715 my $groups = $U->ou_ancestor_setting_value(
716 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
717 for my $grp (@$groups) {
718 return 1 if $self->is_group_descendant($grp, $pid);
723 # returns true if the user is not required to pay rental fees
724 sub is_rental_exempt {
726 my $pid = (ref $self->patron->profile) ?
727 $self->patron->profile->id : $self->patron->profile;
728 my $groups = $U->ou_ancestor_setting_value(
729 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
730 for my $grp (@$groups) {
731 return 1 if $self->is_group_descendant($grp, $pid);
736 sub is_group_descendant {
737 my($self, $p_id, $c_id) = @_;
738 return 0 unless defined $p_id and defined $c_id;
739 return 1 if $c_id == $p_id;
740 while(my $grp = $user_groups{$c_id}) {
741 $c_id = $grp->parent;
742 return 0 unless defined $c_id;
743 return 1 if $c_id == $p_id;
748 sub check_captured_holds {
750 my $copy = $self->copy;
751 my $patron = $self->patron;
753 return undef unless $copy;
755 my $s = $U->copy_status($copy->status)->id;
756 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
757 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
759 # Item is on the holds shelf, make sure it's going to the right person
760 my $holds = $self->editor->search_action_hold_request(
763 current_copy => $copy->id ,
764 capture_time => { '!=' => undef },
765 cancel_time => undef,
766 fulfillment_time => undef
772 if( $holds and $$holds[0] ) {
773 return undef if $$holds[0]->usr == $patron->id;
776 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
778 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
784 my $copy = $self->copy;
787 my $stat = $U->copy_status($copy->status)->id;
789 # We cannot check out a copy if it is in-transit
790 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
791 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
794 $self->handle_claims_returned();
795 return if $self->bail_out;
797 # no claims returned circ was found, check if there is any open circ
798 unless( $self->is_renewal ) {
800 my $circs = $self->editor->search_action_circulation(
801 { target_copy => $copy->id, checkin_time => undef }
804 if(my $old_circ = $circs->[0]) { # an open circ was found
806 my $payload; # event payload
808 if($old_circ->usr == $self->patron->id) {
810 $payload = {old_circ => $old_circ};
812 # If there is an open circulation on the checkout item and an auto-renew
813 # interval is defined, inform the caller that they should go
814 # ahead and renew the item instead of warning about open circulations.
816 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
817 $self->editor->requestor->ws_ou,
818 'circ.checkout_auto_renew_age',
822 if($auto_renew_intvl) {
823 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
824 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($old_circ->xact_start) );
826 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
827 $payload->{auto_renew} = 1;
832 return $self->bail_on_events(
833 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
839 my $LEGACY_CIRC_EVENT_MAP = {
840 'actor.usr.barred' => 'PATRON_BARRED',
841 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
842 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
843 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
844 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
845 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
846 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
847 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
848 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
852 # ---------------------------------------------------------------------
853 # This pushes any patron-related events into the list but does not
854 # set bail_out for any events
855 # ---------------------------------------------------------------------
856 sub run_patron_permit_scripts {
858 my $runner = $self->script_runner;
859 my $patronid = $self->patron->id;
863 if(!$self->legacy_script_support) {
865 my $results = $self->run_indb_circ_test;
866 unless($self->circ_test_success) {
867 push(@allevents, OpenILS::Event->new(
868 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
874 # ---------------------------------------------------------------------
875 # # Now run the patron permit script
876 # ---------------------------------------------------------------------
877 $runner->load($self->circ_permit_patron);
878 my $result = $runner->run or
879 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
881 my $patron_events = $result->{events};
883 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
884 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
885 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
886 $penalties = $penalties->{fatal_penalties};
888 for my $pen (@$penalties) {
889 my $event = OpenILS::Event->new($pen->name);
890 $event->{desc} = $pen->label;
891 push(@allevents, $event);
894 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
897 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
899 $self->push_events(@allevents);
902 sub run_indb_circ_test {
904 return $self->matrix_test_result if $self->matrix_test_result;
906 my $dbfunc = ($self->is_renewal) ?
907 'action.item_user_renew_test' : 'action.item_user_circ_test';
909 my $results = $self->editor->json_query(
912 $self->editor->requestor->ws_ou,
913 ($self->is_precat or $self->is_noncat) ? undef : $self->copy->id,
919 $self->circ_test_success($U->is_true($results->[0]->{success}));
921 if(my $mp = $results->[0]->{matchpoint}) {
922 $self->circ_matrix_matchpoint(
923 $self->editor->retrieve_config_circ_matrix_matchpoint([
926 flesh_fields => {ccmm =>
927 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
933 return $self->matrix_test_result($results);
936 # ---------------------------------------------------------------------
937 # given a use and copy, this will calculate the circulation policy
938 # parameters. Only works with in-db circ.
939 # ---------------------------------------------------------------------
943 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
945 $self->run_indb_circ_test;
948 circ_test_success => $self->circ_test_success,
949 failure_events => [],
953 unless($self->circ_test_success) {
954 push(@{$results->{failure_codes}},
955 $_->{fail_part}) for @{$self->matrix_test_result};
956 push(@{$results->{failure_events}},
957 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part})
958 for @{$self->matrix_test_result};
961 if($self->circ_matrix_matchpoint) {
962 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
963 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
964 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
966 my $policy = $self->get_circ_policy(
967 $duration_rule, $recurring_fine_rule, $max_fine_rule);
969 $$results{$_} = $$policy{$_} for keys %$policy;
975 # ---------------------------------------------------------------------
976 # Loads the circ policy info for duration, recurring fine, and max
977 # fine based on the current copy
978 # ---------------------------------------------------------------------
979 sub get_circ_policy {
980 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
983 duration_rule => $duration_rule->name,
984 recurring_fine_rule => $recurring_fine_rule->name,
985 max_fine_rule => $max_fine_rule->name,
986 max_fine => $self->get_max_fine_amount($max_fine_rule),
987 fine_interval => $recurring_fine_rule->recurance_interval,
988 renewal_remaining => $duration_rule->max_renewals
991 $policy->{duration} = $duration_rule->shrt
992 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
993 $policy->{duration} = $duration_rule->normal
994 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
995 $policy->{duration} = $duration_rule->extended
996 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
998 $policy->{recurring_fine} = $recurring_fine_rule->low
999 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1000 $policy->{recurring_fine} = $recurring_fine_rule->normal
1001 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1002 $policy->{recurring_fine} = $recurring_fine_rule->high
1003 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1008 sub get_max_fine_amount {
1010 my $max_fine_rule = shift;
1011 my $max_amount = $max_fine_rule->amount;
1013 # if is_percent is true then the max->amount is
1014 # use as a percentage of the copy price
1015 if ($U->is_true($max_fine_rule->is_percent)) {
1016 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1017 $max_amount = $price * $max_fine_rule->amount / 100;
1025 sub run_copy_permit_scripts {
1027 my $copy = $self->copy || return;
1028 my $runner = $self->script_runner;
1032 if(!$self->legacy_script_support) {
1033 my $results = $self->run_indb_circ_test;
1034 unless($self->circ_test_success) {
1035 push(@allevents, OpenILS::Event->new(
1036 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
1041 # ---------------------------------------------------------------------
1042 # Capture all of the copy permit events
1043 # ---------------------------------------------------------------------
1044 $runner->load($self->circ_permit_copy);
1045 my $result = $runner->run or
1046 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1047 my $copy_events = $result->{events};
1049 # ---------------------------------------------------------------------
1050 # Now collect all of the events together
1051 # ---------------------------------------------------------------------
1052 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1055 # See if this copy has an alert message
1056 my $ae = $self->check_copy_alert();
1057 push( @allevents, $ae ) if $ae;
1059 # uniquify the events
1060 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1061 @allevents = values %hash;
1064 $_->{payload} = $copy if
1065 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1068 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1070 $self->push_events(@allevents);
1074 sub check_copy_alert {
1076 return undef if $self->is_renewal;
1077 return OpenILS::Event->new(
1078 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1079 if $self->copy and $self->copy->alert_message;
1085 # --------------------------------------------------------------------------
1086 # If the call is overriding and has permissions to override every collected
1087 # event, the are cleared. Any event that the caller does not have
1088 # permission to override, will be left in the event list and bail_out will
1090 # XXX We need code in here to cancel any holds/transits on copies
1091 # that are being force-checked out
1092 # --------------------------------------------------------------------------
1093 sub override_events {
1095 my @events = @{$self->events};
1096 return unless @events;
1098 if(!$self->override) {
1099 return $self->bail_out(1)
1100 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1105 for my $e (@events) {
1106 my $tc = $e->{textcode};
1107 next if $tc eq 'SUCCESS';
1108 my $ov = "$tc.override";
1109 $logger->info("circulator: attempting to override event: $ov");
1111 return $self->bail_on_events($self->editor->event)
1112 unless( $self->editor->allowed($ov) );
1117 # --------------------------------------------------------------------------
1118 # If there is an open claimsreturn circ on the requested copy, close the
1119 # circ if overriding, otherwise bail out
1120 # --------------------------------------------------------------------------
1121 sub handle_claims_returned {
1123 my $copy = $self->copy;
1125 my $CR = $self->editor->search_action_circulation(
1127 target_copy => $copy->id,
1128 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1129 checkin_time => undef,
1133 return unless ($CR = $CR->[0]);
1137 # - If the caller has set the override flag, we will check the item in
1138 if($self->override) {
1140 $CR->checkin_time('now');
1141 $CR->checkin_scan_time('now');
1142 $CR->checkin_lib($self->editor->requestor->ws_ou);
1143 $CR->checkin_workstation($self->editor->requestor->wsid);
1144 $CR->checkin_staff($self->editor->requestor->id);
1146 $evt = $self->editor->event
1147 unless $self->editor->update_action_circulation($CR);
1150 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1153 $self->bail_on_events($evt) if $evt;
1158 # --------------------------------------------------------------------------
1159 # This performs the checkout
1160 # --------------------------------------------------------------------------
1164 $self->log_me("do_checkout()");
1166 # make sure perms are good if this isn't a renewal
1167 unless( $self->is_renewal ) {
1168 return $self->bail_on_events($self->editor->event)
1169 unless( $self->editor->allowed('COPY_CHECKOUT') );
1172 # verify the permit key
1173 unless( $self->check_permit_key ) {
1174 if( $self->permit_override ) {
1175 return $self->bail_on_events($self->editor->event)
1176 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1178 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1182 # if this is a non-cataloged circ, build the circ and finish
1183 if( $self->is_noncat ) {
1184 $self->checkout_noncat;
1186 OpenILS::Event->new('SUCCESS',
1187 payload => { noncat_circ => $self->circ }));
1191 if( $self->is_precat ) {
1192 $self->make_precat_copy;
1193 return if $self->bail_out;
1195 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1196 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1199 $self->do_copy_checks;
1200 return if $self->bail_out;
1202 $self->run_checkout_scripts();
1203 return if $self->bail_out;
1205 $self->build_checkout_circ_object();
1206 return if $self->bail_out;
1208 $self->apply_modified_due_date();
1209 return if $self->bail_out;
1211 return $self->bail_on_events($self->editor->event)
1212 unless $self->editor->create_action_circulation($self->circ);
1214 # refresh the circ to force local time zone for now
1215 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1217 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1219 return if $self->bail_out;
1221 $self->apply_deposit_fee();
1222 return if $self->bail_out;
1224 $self->handle_checkout_holds();
1225 return if $self->bail_out;
1227 # ------------------------------------------------------------------------------
1228 # Update the patron penalty info in the DB. Run it for permit-overrides
1229 # since the penalties are not updated during the permit phase
1230 # ------------------------------------------------------------------------------
1231 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1233 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1236 if($self->is_renewal) {
1237 # flesh the billing summary for the checked-in circ
1238 $pcirc = $self->editor->retrieve_action_circulation([
1240 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1245 OpenILS::Event->new('SUCCESS',
1247 copy => $U->unflesh_copy($self->copy),
1248 circ => $self->circ,
1250 holds_fulfilled => $self->fulfilled_holds,
1251 deposit_billing => $self->deposit_billing,
1252 rental_billing => $self->rental_billing,
1253 parent_circ => $pcirc,
1254 patron => ($self->return_patron) ? $self->patron : undef
1260 sub apply_deposit_fee {
1262 my $copy = $self->copy;
1264 ($self->is_deposit and not $self->is_deposit_exempt) or
1265 ($self->is_rental and not $self->is_rental_exempt);
1267 my $bill = Fieldmapper::money::billing->new;
1268 my $amount = $copy->deposit_amount;
1272 if($self->is_deposit) {
1273 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1275 $self->deposit_billing($bill);
1277 $billing_type = OILS_BILLING_TYPE_RENTAL;
1279 $self->rental_billing($bill);
1282 $bill->xact($self->circ->id);
1283 $bill->amount($amount);
1284 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1285 $bill->billing_type($billing_type);
1286 $bill->btype($btype);
1287 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1289 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1294 my $copy = $self->copy;
1296 my $stat = $copy->status if ref $copy->status;
1297 my $loc = $copy->location if ref $copy->location;
1298 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1300 $copy->status($stat->id) if $stat;
1301 $copy->location($loc->id) if $loc;
1302 $copy->circ_lib($circ_lib->id) if $circ_lib;
1303 $copy->editor($self->editor->requestor->id);
1304 $copy->edit_date('now');
1305 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1307 return $self->bail_on_events($self->editor->event)
1308 unless $self->editor->update_asset_copy($self->copy);
1310 $copy->status($U->copy_status($copy->status));
1311 $copy->location($loc) if $loc;
1312 $copy->circ_lib($circ_lib) if $circ_lib;
1316 sub bail_on_events {
1317 my( $self, @evts ) = @_;
1318 $self->push_events(@evts);
1323 # ------------------------------------------------------------------------------
1324 # When an item is checked out, see if we can fulfill a hold for this patron
1325 # ------------------------------------------------------------------------------
1326 sub handle_checkout_holds {
1328 my $copy = $self->copy;
1329 my $patron = $self->patron;
1331 my $e = $self->editor;
1332 $self->fulfilled_holds([]);
1334 # pre/non-cats can't fulfill a hold
1335 return if $self->is_precat or $self->is_noncat;
1337 my $hold = $e->search_action_hold_request({
1338 current_copy => $copy->id ,
1339 cancel_time => undef,
1340 fulfillment_time => undef,
1342 {expire_time => undef},
1343 {expire_time => {'>' => 'now'}}
1347 if($hold and $hold->usr != $patron->id) {
1348 # reset the hold since the copy is now checked out
1350 $logger->info("circulator: un-targeting hold ".$hold->id.
1351 " because copy ".$copy->id." is getting checked out");
1353 $hold->clear_prev_check_time;
1354 $hold->clear_current_copy;
1355 $hold->clear_capture_time;
1357 return $self->bail_on_event($e->event)
1358 unless $e->update_action_hold_request($hold);
1364 $hold = $self->find_related_user_hold($copy, $patron) or return;
1365 $logger->info("circulator: found related hold to fulfill in checkout");
1368 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1370 # if the hold was never officially captured, capture it.
1371 $hold->current_copy($copy->id);
1372 $hold->capture_time('now') unless $hold->capture_time;
1373 $hold->fulfillment_time('now');
1374 $hold->fulfillment_staff($e->requestor->id);
1375 $hold->fulfillment_lib($e->requestor->ws_ou);
1377 return $self->bail_on_events($e->event)
1378 unless $e->update_action_hold_request($hold);
1380 $holdcode->delete_hold_copy_maps($e, $hold->id);
1381 return $self->fulfilled_holds([$hold->id]);
1385 # ------------------------------------------------------------------------------
1386 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1387 # the patron directly targets the checked out item, see if there is another hold
1388 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1389 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1390 # ------------------------------------------------------------------------------
1391 sub find_related_user_hold {
1392 my($self, $copy, $patron) = @_;
1393 my $e = $self->editor;
1395 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1397 return undef unless $U->ou_ancestor_setting_value(
1398 $e->requestor->ws_ou, 'circ.checkout_fills_related_hold', $e);
1400 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1402 select => {ahr => ['id']},
1407 fkey => 'current_copy',
1408 type => 'left' # there may be no current_copy
1415 fulfillment_time => undef,
1416 cancel_time => undef,
1418 {expire_time => undef},
1419 {expire_time => {'>' => 'now'}}
1426 target => $self->volume->id
1432 target => $self->title->id
1438 {id => undef}, # left-join copy may be nonexistent
1439 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1443 order_by => {ahr => {request_time => {direction => 'asc'}}},
1447 my $hold_info = $e->json_query($args)->[0];
1448 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1453 sub run_checkout_scripts {
1457 my $runner = $self->script_runner;
1466 if(!$self->legacy_script_support) {
1467 $self->run_indb_circ_test();
1468 $duration = $self->circ_matrix_matchpoint->duration_rule;
1469 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1470 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1474 $runner->load($self->circ_duration);
1476 my $result = $runner->run or
1477 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1479 $duration_name = $result->{durationRule};
1480 $recurring_name = $result->{recurringFinesRule};
1481 $max_fine_name = $result->{maxFine};
1484 $duration_name = $duration->name if $duration;
1485 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1488 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1489 return $self->bail_on_events($evt) if $evt;
1491 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1492 return $self->bail_on_events($evt) if $evt;
1494 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1495 return $self->bail_on_events($evt) if $evt;
1500 # The item circulates with an unlimited duration
1506 $self->duration_rule($duration);
1507 $self->recurring_fines_rule($recurring);
1508 $self->max_fine_rule($max_fine);
1512 sub build_checkout_circ_object {
1515 my $circ = Fieldmapper::action::circulation->new;
1516 my $duration = $self->duration_rule;
1517 my $max = $self->max_fine_rule;
1518 my $recurring = $self->recurring_fines_rule;
1519 my $copy = $self->copy;
1520 my $patron = $self->patron;
1524 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1526 my $dname = $duration->name;
1527 my $mname = $max->name;
1528 my $rname = $recurring->name;
1530 $logger->debug("circulator: building circulation ".
1531 "with duration=$dname, maxfine=$mname, recurring=$rname");
1533 $circ->duration($policy->{duration});
1534 $circ->recuring_fine($policy->{recurring_fine});
1535 $circ->duration_rule($duration->name);
1536 $circ->recuring_fine_rule($recurring->name);
1537 $circ->max_fine_rule($max->name);
1538 $circ->max_fine($policy->{max_fine});
1539 $circ->fine_interval($recurring->recurance_interval);
1540 $circ->renewal_remaining($duration->max_renewals);
1544 $logger->info("circulator: copy found with an unlimited circ duration");
1545 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1546 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1547 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1548 $circ->renewal_remaining(0);
1551 $circ->target_copy( $copy->id );
1552 $circ->usr( $patron->id );
1553 $circ->circ_lib( $self->circ_lib );
1554 $circ->workstation($self->editor->requestor->wsid)
1555 if defined $self->editor->requestor->wsid;
1557 # renewals maintain a link to the parent circulation
1558 $circ->parent_circ($self->parent_circ);
1560 if( $self->is_renewal ) {
1561 $circ->opac_renewal('t') if $self->opac_renewal;
1562 $circ->phone_renewal('t') if $self->phone_renewal;
1563 $circ->desk_renewal('t') if $self->desk_renewal;
1564 $circ->renewal_remaining($self->renewal_remaining);
1565 $circ->circ_staff($self->editor->requestor->id);
1569 # if the user provided an overiding checkout time,
1570 # (e.g. the checkout really happened several hours ago), then
1571 # we apply that here. Does this need a perm??
1572 $circ->xact_start(clense_ISO8601($self->checkout_time))
1573 if $self->checkout_time;
1575 # if a patron is renewing, 'requestor' will be the patron
1576 $circ->circ_staff($self->editor->requestor->id);
1577 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1583 sub apply_modified_due_date {
1585 my $circ = $self->circ;
1586 my $copy = $self->copy;
1588 if( $self->due_date ) {
1590 return $self->bail_on_events($self->editor->event)
1591 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1593 $circ->due_date(clense_ISO8601($self->due_date));
1597 # if the due_date lands on a day when the location is closed
1598 return unless $copy and $circ->due_date;
1600 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1602 # due-date overlap should be determined by the location the item
1603 # is checked out from, not the owning or circ lib of the item
1604 my $org = $self->editor->requestor->ws_ou;
1606 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1607 " with an item due date of ".$circ->due_date );
1609 my $dateinfo = $U->storagereq(
1610 'open-ils.storage.actor.org_unit.closed_date.overlap',
1611 $org, $circ->due_date );
1614 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1615 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1617 # XXX make the behavior more dynamic
1618 # for now, we just push the due date to after the close date
1619 $circ->due_date($dateinfo->{end});
1626 sub create_due_date {
1627 my( $self, $duration ) = @_;
1629 # if there is a raw time component (e.g. from postgres),
1630 # turn it into an interval that interval_to_seconds can parse
1631 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1633 # for now, use the server timezone. TODO: use workstation org timezone
1634 my $due_date = DateTime->now(time_zone => 'local');
1636 # add the circ duration
1637 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
1639 # return ISO8601 time with timezone
1640 return $due_date->strftime('%FT%T%z');
1645 sub make_precat_copy {
1647 my $copy = $self->copy;
1650 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1652 $copy->editor($self->editor->requestor->id);
1653 $copy->edit_date('now');
1654 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
1655 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
1656 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
1657 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
1658 $self->update_copy();
1662 $logger->info("circulator: Creating a new precataloged ".
1663 "copy in checkout with barcode " . $self->copy_barcode);
1665 $copy = Fieldmapper::asset::copy->new;
1666 $copy->circ_lib($self->circ_lib);
1667 $copy->creator($self->editor->requestor->id);
1668 $copy->editor($self->editor->requestor->id);
1669 $copy->barcode($self->copy_barcode);
1670 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1671 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1672 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1674 $copy->dummy_title($self->dummy_title || "");
1675 $copy->dummy_author($self->dummy_author || "");
1676 $copy->dummy_isbn($self->dummy_isbn || "");
1677 $copy->circ_modifier($self->circ_modifier);
1680 # See if we need to override the circ_lib for the copy with a configured circ_lib
1681 # Setting is shortname of the org unit
1682 my $precat_circ_lib = $U->ou_ancestor_setting_value(
1683 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
1685 if($precat_circ_lib) {
1686 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
1689 $self->bail_on_events($self->editor->event);
1693 $copy->circ_lib($org->id);
1697 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1699 $self->push_events($self->editor->event);
1703 # this is a little bit of a hack, but we need to
1704 # get the copy into the script runner
1705 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1709 sub checkout_noncat {
1715 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1716 my $count = $self->noncat_count || 1;
1717 my $cotime = clense_ISO8601($self->checkout_time) || "";
1719 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
1723 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1724 $self->editor->requestor->id,
1732 $self->push_events($evt);
1743 $self->log_me("do_checkin()");
1745 return $self->bail_on_events(
1746 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1749 if( $self->checkin_check_holds_shelf() ) {
1750 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1751 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1752 $self->checkin_flesh_events;
1756 unless( $self->is_renewal ) {
1757 return $self->bail_on_events($self->editor->event)
1758 unless $self->editor->allowed('COPY_CHECKIN');
1761 $self->push_events($self->check_copy_alert());
1762 $self->push_events($self->check_checkin_copy_status());
1764 # the renew code will have already found our circulation object
1765 unless( $self->is_renewal and $self->circ ) {
1766 my $circs = $self->editor->search_action_circulation(
1767 { target_copy => $self->copy->id, checkin_time => undef });
1768 $self->circ($$circs[0]);
1770 # for now, just warn if there are multiple open circs on a copy
1771 $logger->warn("circulator: we have ".scalar(@$circs).
1772 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1775 # run the fine generator against this circ, if this circ is there
1776 $self->generate_fines if ($self->circ);
1778 # if the circ is marked as 'claims returned', add the event to the list
1779 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1780 if ($self->circ and $self->circ->stop_fines
1781 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1783 $self->check_circ_deposit();
1785 # handle the overridable events
1786 $self->override_events unless $self->is_renewal;
1787 return if $self->bail_out;
1791 $self->editor->search_action_transit_copy(
1792 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1796 $self->checkin_handle_circ;
1797 return if $self->bail_out;
1798 $self->checkin_changed(1);
1800 } elsif( $self->transit ) {
1801 my $hold_transit = $self->process_received_transit;
1802 $self->checkin_changed(1);
1804 if( $self->bail_out ) {
1805 $self->checkin_flesh_events;
1809 if( my $e = $self->check_checkin_copy_status() ) {
1810 # If the original copy status is special, alert the caller
1811 my $ev = $self->events;
1812 $self->events([$e]);
1813 $self->override_events;
1814 return if $self->bail_out;
1818 if( $hold_transit or
1819 $U->copy_status($self->copy->status)->id
1820 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1823 if( $hold_transit ) {
1824 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1826 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1831 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1833 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1834 $self->reshelve_copy(1);
1835 $self->cancelled_hold_transit(1);
1836 $self->notify_hold(0); # don't notify for cancelled holds
1837 return if $self->bail_out;
1841 # hold transited to correct location
1842 $self->checkin_flesh_events;
1847 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1849 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1850 " that is in-transit, but there is no transit.. repairing");
1851 $self->reshelve_copy(1);
1852 return if $self->bail_out;
1855 if( $self->is_renewal ) {
1856 $self->push_events(OpenILS::Event->new('SUCCESS'));
1860 # ------------------------------------------------------------------------------
1861 # Circulations and transits are now closed where necessary. Now go on to see if
1862 # this copy can fulfill a hold or needs to be routed to a different location
1863 # ------------------------------------------------------------------------------
1865 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1867 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1868 return if $self->bail_out;
1870 unless($needed_for_hold) {
1871 my $circ_lib = (ref $self->copy->circ_lib) ?
1872 $self->copy->circ_lib->id : $self->copy->circ_lib;
1874 if( $self->remote_hold ) {
1875 $circ_lib = $self->remote_hold->pickup_lib;
1876 $logger->warn("circulator: Copy ".$self->copy->barcode.
1877 " is on a remote hold's shelf, sending to $circ_lib");
1880 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1882 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1884 $self->checkin_handle_precat();
1885 return if $self->bail_out;
1889 my $bc = $self->copy->barcode;
1890 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1891 $self->checkin_build_copy_transit($circ_lib);
1892 return if $self->bail_out;
1893 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1898 if($self->claims_never_checked_out and
1899 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
1901 # the item was not supposed to be checked out to the user and should now be marked as missing
1902 $self->copy->status(OILS_COPY_STATUS_MISSING);
1906 $self->reshelve_copy;
1909 return if $self->bail_out;
1911 unless($self->checkin_changed) {
1913 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1914 my $stat = $U->copy_status($self->copy->status)->id;
1916 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1917 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1918 $self->bail_out(1); # no need to commit anything
1922 $self->push_events(OpenILS::Event->new('SUCCESS'))
1923 unless @{$self->events};
1926 OpenILS::Utils::Penalty->calculate_penalties(
1927 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1929 $self->checkin_flesh_events;
1933 # if a deposit was payed for this item, push the event
1934 sub check_circ_deposit {
1936 return unless $self->circ;
1937 my $deposit = $self->editor->search_money_billing(
1939 xact => $self->circ->id,
1941 }, {idlist => 1})->[0];
1943 $self->push_events(OpenILS::Event->new(
1944 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1949 my $force = $self->force || shift;
1950 my $copy = $self->copy;
1952 my $stat = $U->copy_status($copy->status)->id;
1955 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1956 $stat != OILS_COPY_STATUS_CATALOGING and
1957 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1958 $stat != OILS_COPY_STATUS_RESHELVING )) {
1960 $copy->status( OILS_COPY_STATUS_RESHELVING );
1962 $self->checkin_changed(1);
1967 # Returns true if the item is at the current location
1968 # because it was transited there for a hold and the
1969 # hold has not been fulfilled
1970 sub checkin_check_holds_shelf {
1972 return 0 unless $self->copy;
1975 $U->copy_status($self->copy->status)->id ==
1976 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1978 # find the hold that put us on the holds shelf
1979 my $holds = $self->editor->search_action_hold_request(
1981 current_copy => $self->copy->id,
1982 capture_time => { '!=' => undef },
1983 fulfillment_time => undef,
1984 cancel_time => undef,
1989 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1990 $self->reshelve_copy(1);
1994 my $hold = $$holds[0];
1996 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1997 $hold->id. "] for copy ".$self->copy->barcode);
1999 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2000 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2004 $logger->info("circulator: hold is not for here..");
2005 $self->remote_hold($hold);
2010 sub checkin_handle_precat {
2012 my $copy = $self->copy;
2014 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2015 $copy->status(OILS_COPY_STATUS_CATALOGING);
2016 $self->update_copy();
2017 $self->checkin_changed(1);
2018 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2023 sub checkin_build_copy_transit {
2026 my $copy = $self->copy;
2027 my $transit = Fieldmapper::action::transit_copy->new;
2029 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2030 $logger->info("circulator: transiting copy to $dest");
2032 $transit->source($self->editor->requestor->ws_ou);
2033 $transit->dest($dest);
2034 $transit->target_copy($copy->id);
2035 $transit->source_send_time('now');
2036 $transit->copy_status( $U->copy_status($copy->status)->id );
2038 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2040 return $self->bail_on_events($self->editor->event)
2041 unless $self->editor->create_action_transit_copy($transit);
2043 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2045 $self->checkin_changed(1);
2049 # returns true if the item was used (or may potentially be used
2050 # in subsequent calls) to capture a hold.
2051 sub attempt_checkin_hold_capture {
2053 my $copy = $self->copy;
2055 # we've been explicitly told not to capture any holds
2056 return 0 if $self->capture eq 'nocapture';
2058 # See if this copy can fulfill any holds
2059 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2060 $self->editor, $copy, $self->editor->requestor );
2063 $logger->debug("circulator: no potential permitted".
2064 "holds found for copy ".$copy->barcode);
2068 if($self->capture ne 'capture') {
2069 # see if this item is in a hold-capture-delay location
2070 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2071 if($U->is_true($location->hold_verify)) {
2072 $self->bail_on_events(
2073 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2078 $self->retarget($retarget);
2080 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2082 $hold->current_copy($copy->id);
2083 $hold->capture_time('now');
2084 $hold->shelf_time('now')
2085 if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2087 # prevent DB errors caused by fetching
2088 # holds from storage, and updating through cstore
2089 $hold->clear_fulfillment_time;
2090 $hold->clear_fulfillment_staff;
2091 $hold->clear_fulfillment_lib;
2092 $hold->clear_expire_time;
2093 $hold->clear_cancel_time;
2094 $hold->clear_prev_check_time unless $hold->prev_check_time;
2096 $self->bail_on_events($self->editor->event)
2097 unless $self->editor->update_action_hold_request($hold);
2099 $self->checkin_changed(1);
2101 return 0 if $self->bail_out;
2103 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2105 # This hold was captured in the correct location
2106 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2107 $self->push_events(OpenILS::Event->new('SUCCESS'));
2109 #$self->do_hold_notify($hold->id);
2110 $self->notify_hold($hold->id);
2114 # Hold needs to be picked up elsewhere. Build a hold
2115 # transit and route the item.
2116 $self->checkin_build_hold_transit();
2117 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2118 return 0 if $self->bail_out;
2119 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2122 # make sure we save the copy status
2127 sub do_hold_notify {
2128 my( $self, $holdid ) = @_;
2130 my $e = new_editor(xact => 1);
2131 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2133 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2134 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2136 $logger->info("circulator: running delayed hold notify process");
2138 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2139 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2141 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2142 hold_id => $holdid, requestor => $self->editor->requestor);
2144 $logger->debug("circulator: built hold notifier");
2146 if(!$notifier->event) {
2148 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2150 my $stat = $notifier->send_email_notify;
2151 if( $stat == '1' ) {
2152 $logger->info("circulator: hold notify succeeded for hold $holdid");
2156 $logger->warn("circulator: * hold notify failed for hold $holdid");
2159 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2163 sub retarget_holds {
2165 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2166 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2167 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2168 # no reason to wait for the return value
2172 sub checkin_build_hold_transit {
2175 my $copy = $self->copy;
2176 my $hold = $self->hold;
2177 my $trans = Fieldmapper::action::hold_transit_copy->new;
2179 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2181 $trans->hold($hold->id);
2182 $trans->source($self->editor->requestor->ws_ou);
2183 $trans->dest($hold->pickup_lib);
2184 $trans->source_send_time("now");
2185 $trans->target_copy($copy->id);
2187 # when the copy gets to its destination, it will recover
2188 # this status - put it onto the holds shelf
2189 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2191 return $self->bail_on_events($self->editor->event)
2192 unless $self->editor->create_action_hold_transit_copy($trans);
2197 sub process_received_transit {
2199 my $copy = $self->copy;
2200 my $copyid = $self->copy->id;
2202 my $status_name = $U->copy_status($copy->status)->name;
2203 $logger->debug("circulator: attempting transit receive on ".
2204 "copy $copyid. Copy status is $status_name");
2206 my $transit = $self->transit;
2208 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2209 # - this item is in-transit to a different location
2211 my $tid = $transit->id;
2212 my $loc = $self->editor->requestor->ws_ou;
2213 my $dest = $transit->dest;
2215 $logger->info("circulator: Fowarding transit on copy which is destined ".
2216 "for a different location. transit=$tid, copy=$copyid, current ".
2217 "location=$loc, destination location=$dest");
2219 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2221 # grab the associated hold object if available
2222 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2223 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2225 return $self->bail_on_events($evt);
2228 # The transit is received, set the receive time
2229 $transit->dest_recv_time('now');
2230 $self->bail_on_events($self->editor->event)
2231 unless $self->editor->update_action_transit_copy($transit);
2233 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2235 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2236 $copy->status( $transit->copy_status );
2237 $self->update_copy();
2238 return if $self->bail_out;
2242 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2244 # hold has arrived at destination, set shelf time
2245 $hold->shelf_time('now');
2246 $self->bail_on_events($self->editor->event)
2247 unless $self->editor->update_action_hold_request($hold);
2248 return if $self->bail_out;
2250 $self->notify_hold($hold_transit->hold);
2255 OpenILS::Event->new(
2258 payload => { transit => $transit, holdtransit => $hold_transit } ));
2260 return $hold_transit;
2264 sub generate_fines {
2269 my $st = OpenSRF::AppSession->connect('open-ils.storage');
2272 'open-ils.storage.action.circulation.overdue.generate_fines',
2279 # refresh the circ in case the fine generator set the stop_fines field
2280 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
2285 sub checkin_handle_circ {
2287 my $circ = $self->circ;
2288 my $copy = $self->copy;
2292 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2294 # backdate the circ if necessary
2295 if($self->backdate) {
2296 $self->checkin_handle_backdate;
2297 return if $self->bail_out;
2300 if($self->void_overdues) {
2301 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2302 $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2303 return $self->bail_on_events($evt) if $evt;
2306 if(!$circ->stop_fines) {
2307 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2308 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2309 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2310 $circ->stop_fines_time('now');
2311 $circ->stop_fines_time($self->backdate) if $self->backdate;
2314 # see if there are any fines owed on this circ. if not, close it
2315 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2316 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2318 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2320 # Set the checkin vars since we have the item
2321 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2323 # capture the true scan time for back-dated checkins
2324 $circ->checkin_scan_time('now');
2326 $circ->checkin_staff($self->editor->requestor->id);
2327 $circ->checkin_lib($self->editor->requestor->ws_ou);
2328 $circ->checkin_workstation($self->editor->requestor->wsid);
2330 my $circ_lib = (ref $self->copy->circ_lib) ?
2331 $self->copy->circ_lib->id : $self->copy->circ_lib;
2332 my $stat = $U->copy_status($self->copy->status)->id;
2334 # immediately available keeps items lost or missing items from going home before being handled
2335 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2336 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2339 if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2341 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2342 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2344 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2348 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2350 $self->checkin_handle_lost($circ_lib);
2354 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2358 return $self->bail_on_events($self->editor->event)
2359 unless $self->editor->update_action_circulation($circ);
2361 # make sure the circ isn't closed if we just voided some fines
2362 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2363 return $self->bail_on_events($evt) if $evt;
2369 # ------------------------------------------------------------------
2370 # See if we need to void billings for lost checkin
2371 # ------------------------------------------------------------------
2372 sub checkin_handle_lost {
2374 my $circ_lib = shift;
2375 my $circ = $self->circ;
2377 my $max_return = $U->ou_ancestor_setting_value(
2378 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2383 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2384 $tm[5] -= 1 if $tm[5] > 0;
2385 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2387 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2388 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2390 $max_return = 0 if $today < $last_chance;
2393 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2395 my $void_lost = $U->ou_ancestor_setting_value(
2396 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2397 my $void_lost_fee = $U->ou_ancestor_setting_value(
2398 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2399 my $restore_od = $U->ou_ancestor_setting_value(
2400 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2402 $self->checkin_handle_lost_now_found(3) if $void_lost;
2403 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2404 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2407 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2412 sub checkin_handle_backdate {
2415 my $bd = $self->backdate;
2417 # ------------------------------------------------------------------
2418 # clean up the backdate for date comparison
2419 # we want any bills created on or after the backdate
2420 # ------------------------------------------------------------------
2421 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2422 #$bd = "${bd}T23:59:59";
2424 my $bills = $self->editor->search_money_billing(
2426 billing_ts => { '>=' => $bd },
2427 xact => $self->circ->id,
2432 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2434 for my $bill (@$bills) {
2435 unless( $U->is_true($bill->voided) ) {
2436 $logger->info("backdate voiding bill ".$bill->id);
2438 $bill->void_time('now');
2439 $bill->voider($self->editor->requestor->id);
2440 my $n = $bill->note || "";
2441 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2443 $self->bail_on_events($self->editor->event)
2444 unless $self->editor->update_money_billing($bill);
2452 sub find_patron_from_copy {
2454 my $circs = $self->editor->search_action_circulation(
2455 { target_copy => $self->copy->id, checkin_time => undef });
2456 my $circ = $circs->[0];
2457 return unless $circ;
2458 my $u = $self->editor->retrieve_actor_user($circ->usr)
2459 or return $self->bail_on_events($self->editor->event);
2463 sub check_checkin_copy_status {
2465 my $copy = $self->copy;
2471 my $status = $U->copy_status($copy->status)->id;
2474 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2475 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2476 $status == OILS_COPY_STATUS_IN_PROCESS ||
2477 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2478 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2479 $status == OILS_COPY_STATUS_CATALOGING ||
2480 $status == OILS_COPY_STATUS_RESHELVING );
2482 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2483 if( $status == OILS_COPY_STATUS_LOST );
2485 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2486 if( $status == OILS_COPY_STATUS_MISSING );
2488 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2493 # --------------------------------------------------------------------------
2494 # On checkin, we need to return as many relevant objects as we can
2495 # --------------------------------------------------------------------------
2496 sub checkin_flesh_events {
2499 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2500 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2501 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2504 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2507 if($self->hold and !$self->hold->cancel_time) {
2508 $hold = $self->hold;
2509 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
2513 # if we checked in a circulation, flesh the billing summary data
2514 $self->circ->billable_transaction(
2515 $self->editor->retrieve_money_billable_transaction([
2517 {flesh => 1, flesh_fields => {mbt => ['summary']}}
2523 # flesh some patron fields before returning
2525 $self->editor->retrieve_actor_user([
2530 au => ['card', 'billing_address', 'mailing_address']
2537 for my $evt (@{$self->events}) {
2540 $payload->{copy} = $U->unflesh_copy($self->copy);
2541 $payload->{record} = $record,
2542 $payload->{circ} = $self->circ;
2543 $payload->{transit} = $self->transit;
2544 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2545 $payload->{hold} = $hold;
2546 $payload->{patron} = $self->patron;
2547 $evt->{payload} = $payload;
2552 my( $self, $msg ) = @_;
2553 my $bc = ($self->copy) ? $self->copy->barcode :
2556 my $usr = ($self->patron) ? $self->patron->id : "";
2557 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2558 ", recipient=$usr, copy=$bc");
2564 $self->log_me("do_renew()");
2566 # Make sure there is an open circ to renew that is not
2567 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2568 my $usrid = $self->patron->id if $self->patron;
2571 # If we have a patron, match them to the circ
2572 $circ = $self->editor->search_action_circulation(
2573 {target_copy => $self->copy->id, usr => $usrid, stop_fines => undef})->[0];
2575 $circ = $self->editor->search_action_circulation(
2576 {target_copy => $self->copy->id, stop_fines => undef})->[0];
2581 $circ = $self->editor->search_action_circulation(
2582 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2584 $circ = $self->editor->search_action_circulation(
2585 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2589 return $self->bail_on_events($self->editor->event) unless $circ;
2591 # A user is not allowed to renew another user's items without permission
2592 unless( $circ->usr eq $self->editor->requestor->id ) {
2593 return $self->bail_on_events($self->editor->events)
2594 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2597 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2598 if $circ->renewal_remaining < 1;
2600 # -----------------------------------------------------------------
2602 $self->parent_circ($circ->id);
2603 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2606 $self->run_renew_permit;
2609 $self->do_checkin();
2610 return if $self->bail_out;
2612 unless( $self->permit_override ) {
2614 return if $self->bail_out;
2615 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2616 $self->remove_event('ITEM_NOT_CATALOGED');
2619 $self->override_events;
2620 return if $self->bail_out;
2623 $self->do_checkout();
2628 my( $self, $evt ) = @_;
2629 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2630 $logger->debug("circulator: removing event from list: $evt");
2631 my @events = @{$self->events};
2632 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2637 my( $self, $evt ) = @_;
2638 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2639 return grep { $_->{textcode} eq $evt } @{$self->events};
2644 sub run_renew_permit {
2649 if(!$self->legacy_script_support) {
2650 my $results = $self->run_indb_circ_test;
2651 unless($self->circ_test_success) {
2652 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) for @$results;
2657 my $runner = $self->script_runner;
2659 $runner->load($self->circ_permit_renew);
2660 my $result = $runner->run or
2661 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2662 $events = $result->{events};
2663 $self->mk_script_runner;
2666 $logger->activity("circulator: circ_permit_renew for user ".
2667 $self->patron->id." returned events: @$events") if @$events;
2669 $self->push_events(OpenILS::Event->new($_)) for @$events;
2671 $logger->debug("circulator: re-creating script runner to be safe");
2675 sub append_reading_list {
2679 $self->is_checkout and
2684 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2686 # verify history is globally enabled and uses the bucket mechanism
2687 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2688 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2690 unless($htype eq 'bucket') {
2695 # verify the patron wants to retain the hisory
2696 my $setting = $e->search_actor_user_setting(
2697 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2699 unless($setting and $setting->value) {
2704 my $bkt = $e->search_container_copy_bucket(
2705 {owner => $self->patron->id, btype => 'circ_history'})->[0];
2710 # find the next item position
2711 my $last_item = $e->search_container_copy_bucket_item(
2712 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2713 $pos = $last_item->pos + 1 if $last_item;
2716 # create the history bucket if necessary
2717 $bkt = Fieldmapper::container::copy_bucket->new;
2718 $bkt->owner($self->patron->id);
2720 $bkt->btype('circ_history');
2722 $e->create_container_copy_bucket($bkt) or return $e->die_event;
2725 my $item = Fieldmapper::container::copy_bucket_item->new;
2727 $item->bucket($bkt->id);
2728 $item->target_copy($self->copy->id);
2731 $e->create_container_copy_bucket_item($item) or return $e->die_event;
2738 sub make_trigger_events {
2740 return unless $self->circ;
2741 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2742 $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2743 $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2744 $ses->request('open-ils.trigger.event.autocreate', 'renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
2751 sub checkin_handle_lost_now_found {
2752 my ($self, $bill_type) = @_;
2754 # ------------------------------------------------------------------
2755 # remove charge from patron's account if lost item is returned
2756 # ------------------------------------------------------------------
2758 my $bills = $self->editor->search_money_billing(
2760 xact => $self->circ->id,
2765 $logger->debug("voiding lost item charge of ".scalar(@$bills));
2766 for my $bill (@$bills) {
2767 if( !$U->is_true($bill->voided) ) {
2768 $logger->info("lost item returned - voiding bill ".$bill->id);
2770 $bill->void_time('now');
2771 $bill->voider($self->editor->requestor->id);
2772 my $note = ($bill->note) ? $bill->note . "\n" : '';
2773 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2775 $self->bail_on_events($self->editor->event)
2776 unless $self->editor->update_money_billing($bill);
2781 sub checkin_handle_lost_now_found_restore_od {
2784 # ------------------------------------------------------------------
2785 # restore those overdue charges voided when item was set to lost
2786 # ------------------------------------------------------------------
2788 my $ods = $self->editor->search_money_billing(
2790 xact => $self->circ->id,
2795 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
2796 for my $bill (@$ods) {
2797 if( $U->is_true($bill->voided) ) {
2798 $logger->info("lost item returned - restoring overdue ".$bill->id);
2800 $bill->clear_void_time;
2801 $bill->voider($self->editor->requestor->id);
2802 my $note = ($bill->note) ? $bill->note . "\n" : '';
2803 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2805 $self->bail_on_events($self->editor->event)
2806 unless $self->editor->update_money_billing($bill);