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";
16 return 1 if ($v == 1);
17 return 1 if ($v =~ /^t/io);
18 return 1 if ($v =~ /^y/io);
25 my $conf = OpenSRF::Utils::SettingsClient->new;
26 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
27 my @pfx = ( @pfx2, "scripts" );
29 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
30 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
31 my $d = $conf->config_value( @pfx, 'circ_duration' );
32 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
33 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
34 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
35 my $lb = $conf->config_value( @pfx2, 'script_path' );
37 $logger->error( "Missing circ script(s)" )
38 unless( $p and $c and $d and $f and $m and $pr );
40 $scripts{circ_permit_patron} = $p;
41 $scripts{circ_permit_copy} = $c;
42 $scripts{circ_duration} = $d;
43 $scripts{circ_recurring_fines}= $f;
44 $scripts{circ_max_fines} = $m;
45 $scripts{circ_permit_renew} = $pr;
47 $lb = [ $lb ] unless ref($lb);
51 "circulator: Loaded rules scripts for circ: " .
52 "circ permit patron = $p, ".
53 "circ permit copy = $c, ".
54 "circ duration = $d, ".
55 "circ recurring fines = $f, " .
56 "circ max fines = $m, ".
57 "circ renew permit = $pr. ".
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");
152 my( $self, $conn, $auth, $args ) = @_;
153 translate_legacy_args($args);
154 my $api = $self->api_name;
157 OpenILS::Application::Circ::Circulator->new($auth, %$args);
159 return circ_events($circulator) if $circulator->bail_out;
161 # --------------------------------------------------------------------------
162 # Go ahead and load the script runner to make sure we have all
163 # of the objects we need
164 # --------------------------------------------------------------------------
165 $circulator->is_renewal(1) if $api =~ /renew/;
166 $circulator->is_checkin(1) if $api =~ /checkin/;
167 $circulator->check_penalty_on_renew(1) if
168 $circulator->is_renewal and $U->ou_ancestor_setting_value(
169 $circulator->editor->requestor->ws_ou, 'circ.renew.check_penalty', $circulator->editor);
170 $circulator->mk_script_runner;
171 return circ_events($circulator) if $circulator->bail_out;
173 $circulator->circ_permit_patron($scripts{circ_permit_patron});
174 $circulator->circ_permit_copy($scripts{circ_permit_copy});
175 $circulator->circ_duration($scripts{circ_duration});
176 $circulator->circ_permit_renew($scripts{circ_permit_renew});
178 $circulator->override(1) if $api =~ /override/o;
180 if( $api =~ /checkout\.permit/ ) {
181 $circulator->do_permit();
183 } elsif( $api =~ /checkout.full/ ) {
185 $circulator->do_permit();
186 unless( $circulator->bail_out ) {
187 $circulator->events([]);
188 $circulator->do_checkout();
191 } elsif( $api =~ /checkout/ ) {
192 $circulator->do_checkout();
194 } elsif( $api =~ /checkin/ ) {
195 $circulator->do_checkin();
197 } elsif( $api =~ /renew/ ) {
198 $circulator->is_renewal(1);
199 $circulator->do_renew();
202 if( $circulator->bail_out ) {
205 # make sure no success event accidentally slip in
207 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
210 my @e = @{$circulator->events};
211 push( @ee, $_->{textcode} ) for @e;
212 $logger->info("circulator: bailing out with events: @ee");
214 $circulator->editor->rollback;
217 $circulator->editor->commit;
220 $circulator->script_runner->cleanup;
222 $conn->respond_complete(circ_events($circulator));
224 unless($circulator->bail_out) {
225 $circulator->do_hold_notify($circulator->notify_hold)
226 if $circulator->notify_hold;
227 $circulator->retarget_holds if $circulator->retarget;
233 my @e = @{$circ->events};
234 # if we have multiple events, SUCCESS should not be one of them;
235 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
236 return (@e == 1) ? $e[0] : \@e;
241 sub translate_legacy_args {
244 if( $$args{barcode} ) {
245 $$args{copy_barcode} = $$args{barcode};
246 delete $$args{barcode};
249 if( $$args{copyid} ) {
250 $$args{copy_id} = $$args{copyid};
251 delete $$args{copyid};
254 if( $$args{patronid} ) {
255 $$args{patron_id} = $$args{patronid};
256 delete $$args{patronid};
259 if( $$args{patron} and !ref($$args{patron}) ) {
260 $$args{patron_id} = $$args{patron};
261 delete $$args{patron};
265 if( $$args{noncat} ) {
266 $$args{is_noncat} = $$args{noncat};
267 delete $$args{noncat};
270 if( $$args{precat} ) {
271 $$args{is_precat} = $$args{precat};
272 delete $$args{precat};
278 # --------------------------------------------------------------------------
279 # This package actually manages all of the circulation logic
280 # --------------------------------------------------------------------------
281 package OpenILS::Application::Circ::Circulator;
282 use strict; use warnings;
283 use vars q/$AUTOLOAD/;
285 use OpenILS::Utils::Fieldmapper;
286 use OpenSRF::Utils::Cache;
287 use Digest::MD5 qw(md5_hex);
288 use DateTime::Format::ISO8601;
289 use OpenILS::Utils::PermitHold;
290 use OpenSRF::Utils qw/:datetime/;
291 use OpenSRF::Utils::SettingsClient;
292 use OpenILS::Application::Circ::Holds;
293 use OpenILS::Application::Circ::Transit;
294 use OpenSRF::Utils::Logger qw(:logger);
295 use OpenILS::Utils::CStoreEditor qw/:funcs/;
296 use OpenILS::Application::Circ::ScriptBuilder;
297 use OpenILS::Const qw/:const/;
299 my $holdcode = "OpenILS::Application::Circ::Holds";
300 my $transcode = "OpenILS::Application::Circ::Transit";
305 # --------------------------------------------------------------------------
306 # Add a pile of automagic getter/setter methods
307 # --------------------------------------------------------------------------
308 my @AUTOLOAD_FIELDS = qw/
323 check_penalty_on_renew
350 recurring_fines_level
363 cancelled_hold_transit
373 my $type = ref($self) or die "$self is not an object";
375 my $name = $AUTOLOAD;
378 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
379 $logger->error("circulator: $type: invalid autoload field: $name");
380 die "$type: invalid autoload field: $name\n"
385 *{"${type}::${name}"} = sub {
388 $s->{$name} = $v if defined $v;
392 return $self->$name($data);
397 my( $class, $auth, %args ) = @_;
398 $class = ref($class) || $class;
399 my $self = bless( {}, $class );
403 new_editor(xact => 1, authtoken => $auth) );
405 unless( $self->editor->checkauth ) {
406 $self->bail_on_events($self->editor->event);
410 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
412 $self->$_($args{$_}) for keys %args;
415 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
417 # if this is a renewal, default to desk_renewal
418 $self->desk_renewal(1) unless
419 $self->opac_renewal or $self->phone_renewal;
425 # --------------------------------------------------------------------------
426 # True if we should discontinue processing
427 # --------------------------------------------------------------------------
429 my( $self, $bool ) = @_;
430 if( defined $bool ) {
431 $logger->info("circulator: BAILING OUT") if $bool;
432 $self->{bail_out} = $bool;
434 return $self->{bail_out};
439 my( $self, @evts ) = @_;
442 $logger->info("circulator: pushing event ".$e->{textcode});
443 push( @{$self->events}, $e ) unless
444 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
450 my $key = md5_hex( time() . rand() . "$$" );
451 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
452 return $self->permit_key($key);
455 sub check_permit_key {
457 my $key = $self->permit_key;
458 return 0 unless $key;
459 my $k = "oils_permit_key_$key";
460 my $one = $self->cache_handle->get_cache($k);
461 $self->cache_handle->delete_cache($k);
462 return ($one) ? 1 : 0;
466 # --------------------------------------------------------------------------
467 # This builds the script runner environment and fetches most of the
469 # --------------------------------------------------------------------------
470 sub mk_script_runner {
476 qw/copy copy_barcode copy_id patron
477 patron_id patron_barcode volume title editor/;
479 # Translate our objects into the ScriptBuilder args hash
480 $$args{$_} = $self->$_() for @fields;
482 $args->{ignore_user_status} = 1 if $self->is_checkin;
483 $$args{fetch_patron_by_circ_copy} = 1;
484 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
486 if( my $pco = $self->pending_checkouts ) {
487 $logger->info("circulator: we were given a pending checkouts number of $pco");
488 $$args{patronItemsOut} = $pco;
491 # This fetches most of the objects we need
492 $self->script_runner(
493 OpenILS::Application::Circ::ScriptBuilder->build($args));
495 # Now we translate the ScriptBuilder objects back into self
496 $self->$_($$args{$_}) for @fields;
498 my @evts = @{$args->{_events}} if $args->{_events};
500 $logger->debug("circulator: script builder returned events: @evts") if @evts;
504 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
505 if(!$self->is_noncat and
507 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
511 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
512 return $self->bail_on_events(@e);
516 $self->is_precat(1) if $self->copy
517 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
519 # We can't renew if there is no copy
520 return $self->bail_on_events(@evts) if
521 $self->is_renewal and !$self->copy;
523 # Set some circ-specific flags in the script environment
524 my $evt = "environment";
525 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
527 if( $self->is_noncat ) {
528 $self->script_runner->insert("$evt.isNonCat", 1);
529 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
532 if( $self->is_precat ) {
533 $self->script_runner->insert("environment.isPrecat", 1, 1);
536 $self->script_runner->add_path( $_ ) for @$script_libs;
544 # --------------------------------------------------------------------------
545 # Does the circ permit work
546 # --------------------------------------------------------------------------
550 $self->log_me("do_permit()");
552 unless( $self->editor->requestor->id == $self->patron->id ) {
553 return $self->bail_on_events($self->editor->event)
554 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
557 $self->check_captured_holds();
558 $self->do_copy_checks();
559 return if $self->bail_out;
560 $self->run_patron_permit_scripts();
561 $self->run_copy_permit_scripts()
562 unless $self->is_precat or $self->is_noncat;
563 $self->override_events() unless
564 $self->is_renewal and not $self->check_penalty_on_renew;
565 return if $self->bail_out;
567 if( $self->is_precat ) {
570 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
571 return $self->bail_out(1) unless $self->is_renewal;
577 payload => $self->mk_permit_key));
581 sub check_captured_holds {
583 my $copy = $self->copy;
584 my $patron = $self->patron;
586 return undef unless $copy;
588 my $s = $U->copy_status($copy->status)->id;
589 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
590 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
592 # Item is on the holds shelf, make sure it's going to the right person
593 my $holds = $self->editor->search_action_hold_request(
596 current_copy => $copy->id ,
597 capture_time => { '!=' => undef },
598 cancel_time => undef,
599 fulfillment_time => undef
605 if( $holds and $$holds[0] ) {
606 return undef if $$holds[0]->usr == $patron->id;
609 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
611 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
617 my $copy = $self->copy;
620 my $stat = $U->copy_status($copy->status)->id;
622 # We cannot check out a copy if it is in-transit
623 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
624 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
627 $self->handle_claims_returned();
628 return if $self->bail_out;
630 # no claims returned circ was found, check if there is any open circ
631 unless( $self->is_renewal ) {
632 my $circs = $self->editor->search_action_circulation(
633 { target_copy => $copy->id, checkin_time => undef }
636 return $self->bail_on_events(
637 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
642 sub send_penalty_request {
644 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
645 $self->penalty_request(
647 'open-ils.penalty.patron_penalty.calculate',
649 authtoken => $self->editor->authtoken,
650 patron => $self->patron } ) );
653 sub gather_penalty_request {
655 return [] unless $self->penalty_request;
656 my $data = $self->penalty_request->recv;
658 throw $data if UNIVERSAL::isa($data,'Error');
659 $data = $data->content;
660 return $data->{fatal_penalties};
662 $logger->error("circulator: penalty request returned no data");
666 # ---------------------------------------------------------------------
667 # This pushes any patron-related events into the list but does not
668 # set bail_out for any events
669 # ---------------------------------------------------------------------
670 sub run_patron_permit_scripts {
672 my $runner = $self->script_runner;
673 my $patronid = $self->patron->id;
675 $self->send_penalty_request() unless
676 $self->is_renewal and not $self->check_penalty_on_renew;
679 # ---------------------------------------------------------------------
680 # Now run the patron permit script
681 # ---------------------------------------------------------------------
682 $runner->load($self->circ_permit_patron);
683 my $result = $runner->run or
684 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
686 my $patron_events = $result->{events};
689 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
690 [] : $self->gather_penalty_request();
692 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
694 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
696 $self->push_events(@allevents);
700 sub run_copy_permit_scripts {
702 my $copy = $self->copy || return;
703 my $runner = $self->script_runner;
705 # ---------------------------------------------------------------------
706 # Capture all of the copy permit events
707 # ---------------------------------------------------------------------
708 $runner->load($self->circ_permit_copy);
709 my $result = $runner->run or
710 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
711 my $copy_events = $result->{events};
713 # ---------------------------------------------------------------------
714 # Now collect all of the events together
715 # ---------------------------------------------------------------------
717 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
719 # See if this copy has an alert message
720 my $ae = $self->check_copy_alert();
721 push( @allevents, $ae ) if $ae;
723 # uniquify the events
724 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
725 @allevents = values %hash;
728 $_->{payload} = $copy if
729 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
732 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
734 $self->push_events(@allevents);
738 sub check_copy_alert {
740 return undef if $self->is_renewal;
741 return OpenILS::Event->new(
742 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
743 if $self->copy and $self->copy->alert_message;
749 # --------------------------------------------------------------------------
750 # If the call is overriding and has permissions to override every collected
751 # event, the are cleared. Any event that the caller does not have
752 # permission to override, will be left in the event list and bail_out will
754 # XXX We need code in here to cancel any holds/transits on copies
755 # that are being force-checked out
756 # --------------------------------------------------------------------------
757 sub override_events {
759 my @events = @{$self->events};
760 return unless @events;
762 if(!$self->override) {
763 return $self->bail_out(1)
764 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
769 for my $e (@events) {
770 my $tc = $e->{textcode};
771 next if $tc eq 'SUCCESS';
772 my $ov = "$tc.override";
773 $logger->info("circulator: attempting to override event: $ov");
775 return $self->bail_on_events($self->editor->event)
776 unless( $self->editor->allowed($ov) );
781 # --------------------------------------------------------------------------
782 # If there is an open claimsreturn circ on the requested copy, close the
783 # circ if overriding, otherwise bail out
784 # --------------------------------------------------------------------------
785 sub handle_claims_returned {
787 my $copy = $self->copy;
789 my $CR = $self->editor->search_action_circulation(
791 target_copy => $copy->id,
792 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
793 checkin_time => undef,
797 return unless ($CR = $CR->[0]);
801 # - If the caller has set the override flag, we will check the item in
802 if($self->override) {
804 $CR->checkin_time('now');
805 $CR->checkin_lib($self->editor->requestor->ws_ou);
806 $CR->checkin_staff($self->editor->requestor->id);
808 $evt = $self->editor->event
809 unless $self->editor->update_action_circulation($CR);
812 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
815 $self->bail_on_events($evt) if $evt;
820 # --------------------------------------------------------------------------
821 # This performs the checkout
822 # --------------------------------------------------------------------------
826 $self->log_me("do_checkout()");
828 # make sure perms are good if this isn't a renewal
829 unless( $self->is_renewal ) {
830 return $self->bail_on_events($self->editor->event)
831 unless( $self->editor->allowed('COPY_CHECKOUT') );
834 # verify the permit key
835 unless( $self->check_permit_key ) {
836 if( $self->permit_override ) {
837 return $self->bail_on_events($self->editor->event)
838 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
840 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
844 # if this is a non-cataloged circ, build the circ and finish
845 if( $self->is_noncat ) {
846 $self->checkout_noncat;
848 OpenILS::Event->new('SUCCESS',
849 payload => { noncat_circ => $self->circ }));
853 if( $self->is_precat ) {
854 $self->script_runner->insert("environment.isPrecat", 1, 1);
855 $self->make_precat_copy;
856 return if $self->bail_out;
858 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
859 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
862 $self->do_copy_checks;
863 return if $self->bail_out;
865 $self->run_checkout_scripts();
866 return if $self->bail_out;
868 $self->build_checkout_circ_object();
869 return if $self->bail_out;
871 $self->apply_modified_due_date();
872 return if $self->bail_out;
874 return $self->bail_on_events($self->editor->event)
875 unless $self->editor->create_action_circulation($self->circ);
877 # refresh the circ to force local time zone for now
878 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
880 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
882 return if $self->bail_out;
884 $self->handle_checkout_holds();
885 return if $self->bail_out;
887 # ------------------------------------------------------------------------------
888 # Update the patron penalty info in the DB. Run it for permit-overrides or
889 # renewals since both of those cases do not require the penalty server to
890 # run during the permit phase of the checkout
891 # ------------------------------------------------------------------------------
892 if( $self->permit_override or $self->is_renewal ) {
893 $U->update_patron_penalties(
894 authtoken => $self->editor->authtoken,
895 patron => $self->patron,
900 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
902 OpenILS::Event->new('SUCCESS',
904 copy => $U->unflesh_copy($self->copy),
907 holds_fulfilled => $self->fulfilled_holds,
915 my $copy = $self->copy;
917 my $stat = $copy->status if ref $copy->status;
918 my $loc = $copy->location if ref $copy->location;
919 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
921 $copy->status($stat->id) if $stat;
922 $copy->location($loc->id) if $loc;
923 $copy->circ_lib($circ_lib->id) if $circ_lib;
924 $copy->editor($self->editor->requestor->id);
925 $copy->edit_date('now');
926 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
928 return $self->bail_on_events($self->editor->event)
929 unless $self->editor->update_asset_copy($self->copy);
931 $copy->status($U->copy_status($copy->status));
932 $copy->location($loc) if $loc;
933 $copy->circ_lib($circ_lib) if $circ_lib;
938 my( $self, @evts ) = @_;
939 $self->push_events(@evts);
943 sub handle_checkout_holds {
946 my $copy = $self->copy;
947 my $patron = $self->patron;
949 my $holds = $self->editor->search_action_hold_request(
951 current_copy => $copy->id ,
952 cancel_time => undef,
953 fulfillment_time => undef
959 # XXX We should only fulfill one hold here...
960 # XXX If a hold was transited to the user who is checking out
961 # the item, we need to make sure that hold is what's grabbed
964 # for now, just sort by id to get what should be the oldest hold
965 $holds = [ sort { $a->id <=> $b->id } @$holds ];
966 my @myholds = grep { $_->usr eq $patron->id } @$holds;
967 my @altholds = grep { $_->usr ne $patron->id } @$holds;
970 my $hold = $myholds[0];
972 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
974 # if the hold was never officially captured, capture it.
975 $hold->capture_time('now') unless $hold->capture_time;
977 # just make sure it's set correctly
978 $hold->current_copy($copy->id);
980 $hold->fulfillment_time('now');
981 $hold->fulfillment_staff($self->editor->requestor->id);
982 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
984 return $self->bail_on_events($self->editor->event)
985 unless $self->editor->update_action_hold_request($hold);
987 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
989 push( @fulfilled, $hold->id );
992 # If there are any holds placed for other users that point to this copy,
993 # then we need to un-target those holds so the targeter can pick a new copy
996 $logger->info("circulator: un-targeting hold ".$_->id.
997 " because copy ".$copy->id." is getting checked out");
999 # - make the targeter process this hold at next run
1000 $_->clear_prev_check_time;
1002 # - clear out the targetted copy
1003 $_->clear_current_copy;
1004 $_->clear_capture_time;
1006 return $self->bail_on_event($self->editor->event)
1007 unless $self->editor->update_action_hold_request($_);
1011 $self->fulfilled_holds(\@fulfilled);
1016 sub run_checkout_scripts {
1020 my $runner = $self->script_runner;
1021 $runner->load($self->circ_duration);
1023 my $result = $runner->run or
1024 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1026 my $duration = $result->{durationRule};
1027 my $recurring = $result->{recurringFinesRule};
1028 my $max_fine = $result->{maxFine};
1030 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1032 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1033 return $self->bail_on_events($evt) if $evt;
1035 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1036 return $self->bail_on_events($evt) if $evt;
1038 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1039 return $self->bail_on_events($evt) if $evt;
1043 # The item circulates with an unlimited duration
1049 $self->duration_rule($duration);
1050 $self->recurring_fines_rule($recurring);
1051 $self->max_fine_rule($max_fine);
1055 sub build_checkout_circ_object {
1058 my $circ = Fieldmapper::action::circulation->new;
1059 my $duration = $self->duration_rule;
1060 my $max = $self->max_fine_rule;
1061 my $recurring = $self->recurring_fines_rule;
1062 my $copy = $self->copy;
1063 my $patron = $self->patron;
1067 my $dname = $duration->name;
1068 my $mname = $max->name;
1069 my $rname = $recurring->name;
1071 my $max_amount = $max->amount;
1073 # if is_percent is true then the max->amount is
1074 # use as a percentage of the copy price
1075 if (isTrue($max->is_percent)) {
1077 my $cn = $self->editor->retrieve_asset_call_number($copy->call_number);
1079 my $default_price = $U->ou_ancestor_setting_value(
1080 $cn->owning_lib, OILS_SETTING_DEF_ITEM_PRICE, $self->editor) || 0;
1081 my $charge_on_0 = $U->ou_ancestor_setting_value(
1082 $cn->owning_lib, OILS_SETTING_CHARGE_LOST_ON_ZERO, $self->editor) || 0;
1084 # Find the most appropriate "price" -- same definition as the
1085 # LOST price. See OpenILS::Circ::new_set_circ_lost
1086 $max_amount = $copy->price;
1087 $max_amount = $default_price unless defined $max_amount;
1088 $max_amount = 0 if $max_amount < 0;
1089 $max_amount = $default_price if $max_amount == 0 and $charge_on_0;
1091 $max_amount *= $max->amount / 100;
1095 $logger->debug("circulator: building circulation ".
1096 "with duration=$dname, maxfine=$mname, recurring=$rname");
1098 $circ->duration( $duration->shrt )
1099 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1100 $circ->duration( $duration->normal )
1101 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1102 $circ->duration( $duration->extended )
1103 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1105 $circ->recuring_fine( $recurring->low )
1106 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1107 $circ->recuring_fine( $recurring->normal )
1108 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1109 $circ->recuring_fine( $recurring->high )
1110 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1112 $circ->duration_rule( $duration->name );
1113 $circ->recuring_fine_rule( $recurring->name );
1114 $circ->max_fine_rule( $max->name );
1116 $circ->max_fine( $max_amount );
1118 $circ->fine_interval($recurring->recurance_interval);
1119 $circ->renewal_remaining( $duration->max_renewals );
1123 $logger->info("circulator: copy found with an unlimited circ duration");
1124 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1125 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1126 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1127 $circ->renewal_remaining(0);
1130 $circ->target_copy( $copy->id );
1131 $circ->usr( $patron->id );
1132 $circ->circ_lib( $self->circ_lib );
1134 if( $self->is_renewal ) {
1135 $circ->opac_renewal('t') if $self->opac_renewal;
1136 $circ->phone_renewal('t') if $self->phone_renewal;
1137 $circ->desk_renewal('t') if $self->desk_renewal;
1138 $circ->renewal_remaining($self->renewal_remaining);
1139 $circ->circ_staff($self->editor->requestor->id);
1143 # if the user provided an overiding checkout time,
1144 # (e.g. the checkout really happened several hours ago), then
1145 # we apply that here. Does this need a perm??
1146 $circ->xact_start(clense_ISO8601($self->checkout_time))
1147 if $self->checkout_time;
1149 # if a patron is renewing, 'requestor' will be the patron
1150 $circ->circ_staff($self->editor->requestor->id);
1151 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1157 sub apply_modified_due_date {
1159 my $circ = $self->circ;
1160 my $copy = $self->copy;
1162 if( $self->due_date ) {
1164 return $self->bail_on_events($self->editor->event)
1165 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1167 $circ->due_date(clense_ISO8601($self->due_date));
1171 # if the due_date lands on a day when the location is closed
1172 return unless $copy and $circ->due_date;
1174 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1176 # due-date overlap should be determined by the location the item
1177 # is checked out from, not the owning or circ lib of the item
1178 my $org = $self->editor->requestor->ws_ou;
1180 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1181 " with an item due date of ".$circ->due_date );
1183 my $dateinfo = $U->storagereq(
1184 'open-ils.storage.actor.org_unit.closed_date.overlap',
1185 $org, $circ->due_date );
1188 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1189 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1191 # XXX make the behavior more dynamic
1192 # for now, we just push the due date to after the close date
1193 $circ->due_date($dateinfo->{end});
1200 sub create_due_date {
1201 my( $self, $duration ) = @_;
1202 # if there is a raw time component (e.g. from postgres),
1203 # turn it into an interval that interval_to_seconds can parse
1204 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1205 my ($sec,$min,$hour,$mday,$mon,$year) =
1206 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1207 $year += 1900; $mon += 1;
1208 my $due_date = sprintf(
1209 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1210 $year, $mon, $mday, $hour, $min, $sec);
1216 sub make_precat_copy {
1218 my $copy = $self->copy;
1221 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1223 $copy->editor($self->editor->requestor->id);
1224 $copy->edit_date('now');
1225 $copy->dummy_title($self->dummy_title);
1226 $copy->dummy_author($self->dummy_author);
1228 $self->update_copy();
1232 $logger->info("circulator: Creating a new precataloged ".
1233 "copy in checkout with barcode " . $self->copy_barcode);
1235 $copy = Fieldmapper::asset::copy->new;
1236 $copy->circ_lib($self->circ_lib);
1237 $copy->creator($self->editor->requestor->id);
1238 $copy->editor($self->editor->requestor->id);
1239 $copy->barcode($self->copy_barcode);
1240 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1241 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1242 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1244 $copy->dummy_title($self->dummy_title || "");
1245 $copy->dummy_author($self->dummy_author || "");
1247 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1249 $self->push_events($self->editor->event);
1253 # this is a little bit of a hack, but we need to
1254 # get the copy into the script runner
1255 $self->script_runner->insert("environment.copy", $copy, 1);
1259 sub checkout_noncat {
1265 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1266 my $count = $self->noncat_count || 1;
1267 my $cotime = clense_ISO8601($self->checkout_time) || "";
1269 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1273 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1274 $self->editor->requestor->id,
1282 $self->push_events($evt);
1293 $self->log_me("do_checkin()");
1296 return $self->bail_on_events(
1297 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1300 if( $self->checkin_check_holds_shelf() ) {
1301 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1302 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1303 $self->checkin_flesh_events;
1307 unless( $self->is_renewal ) {
1308 return $self->bail_on_events($self->editor->event)
1309 unless $self->editor->allowed('COPY_CHECKIN');
1312 $self->push_events($self->check_copy_alert());
1313 $self->push_events($self->check_checkin_copy_status());
1315 # the renew code will have already found our circulation object
1316 unless( $self->is_renewal and $self->circ ) {
1317 my $circs = $self->editor->search_action_circulation(
1318 { target_copy => $self->copy->id, checkin_time => undef });
1319 $self->circ($$circs[0]);
1321 # for now, just warn if there are multiple open circs on a copy
1322 $logger->warn("circulator: we have ".scalar(@$circs).
1323 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1326 # if the circ is marked as 'claims returned', add the event to the list
1327 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1328 if ($self->circ and $self->circ->stop_fines
1329 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1331 # handle the overridable events
1332 $self->override_events unless $self->is_renewal;
1333 return if $self->bail_out;
1337 $self->editor->search_action_transit_copy(
1338 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1342 $self->checkin_handle_circ;
1343 return if $self->bail_out;
1344 $self->checkin_changed(1);
1346 } elsif( $self->transit ) {
1347 my $hold_transit = $self->process_received_transit;
1348 $self->checkin_changed(1);
1350 if( $self->bail_out ) {
1351 $self->checkin_flesh_events;
1355 if( my $e = $self->check_checkin_copy_status() ) {
1356 # If the original copy status is special, alert the caller
1357 my $ev = $self->events;
1358 $self->events([$e]);
1359 $self->override_events;
1360 return if $self->bail_out;
1364 if( $hold_transit or
1365 $U->copy_status($self->copy->status)->id
1366 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1369 if( $hold_transit ) {
1370 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1372 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1377 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1379 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1380 $self->reshelve_copy(1);
1381 $self->cancelled_hold_transit(1);
1382 $self->notify_hold(0); # don't notify for cancelled holds
1383 return if $self->bail_out;
1387 # hold transited to correct location
1388 $self->checkin_flesh_events;
1393 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1395 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1396 " that is in-transit, but there is no transit.. repairing");
1397 $self->reshelve_copy(1);
1398 return if $self->bail_out;
1401 if( $self->is_renewal ) {
1402 $self->push_events(OpenILS::Event->new('SUCCESS'));
1406 # ------------------------------------------------------------------------------
1407 # Circulations and transits are now closed where necessary. Now go on to see if
1408 # this copy can fulfill a hold or needs to be routed to a different location
1409 # ------------------------------------------------------------------------------
1411 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1412 return if $self->bail_out;
1414 } else { # not needed for a hold
1416 my $circ_lib = (ref $self->copy->circ_lib) ?
1417 $self->copy->circ_lib->id : $self->copy->circ_lib;
1419 if( $self->remote_hold ) {
1420 $circ_lib = $self->remote_hold->pickup_lib;
1421 $logger->warn("circulator: Copy ".$self->copy->barcode.
1422 " is on a remote hold's shelf, sending to $circ_lib");
1425 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1427 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1429 $self->checkin_handle_precat();
1430 return if $self->bail_out;
1434 my $bc = $self->copy->barcode;
1435 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1436 $self->checkin_build_copy_transit($circ_lib);
1437 return if $self->bail_out;
1438 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1442 $self->reshelve_copy;
1443 return if $self->bail_out;
1445 unless($self->checkin_changed) {
1447 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1448 my $stat = $U->copy_status($self->copy->status)->id;
1450 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1451 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1452 $self->bail_out(1); # no need to commit anything
1456 $self->push_events(OpenILS::Event->new('SUCCESS'))
1457 unless @{$self->events};
1461 # ------------------------------------------------------------------------------
1462 # Update the patron penalty info in the DB
1463 # ------------------------------------------------------------------------------
1464 $U->update_patron_penalties(
1465 authtoken => $self->editor->authtoken,
1466 patron => $self->patron,
1467 background => 1 ) if $self->is_checkin;
1469 $self->checkin_flesh_events;
1475 my $force = $self->force || shift;
1476 my $copy = $self->copy;
1478 my $stat = $U->copy_status($copy->status)->id;
1481 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1482 $stat != OILS_COPY_STATUS_CATALOGING and
1483 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1484 $stat != OILS_COPY_STATUS_RESHELVING )) {
1486 $copy->status( OILS_COPY_STATUS_RESHELVING );
1488 $self->checkin_changed(1);
1493 # Returns true if the item is at the current location
1494 # because it was transited there for a hold and the
1495 # hold has not been fulfilled
1496 sub checkin_check_holds_shelf {
1498 return 0 unless $self->copy;
1501 $U->copy_status($self->copy->status)->id ==
1502 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1504 # find the hold that put us on the holds shelf
1505 my $holds = $self->editor->search_action_hold_request(
1507 current_copy => $self->copy->id,
1508 capture_time => { '!=' => undef },
1509 fulfillment_time => undef,
1510 cancel_time => undef,
1515 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1516 $self->reshelve_copy(1);
1520 my $hold = $$holds[0];
1522 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1523 $hold->id. "] for copy ".$self->copy->barcode);
1525 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1526 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1530 $logger->info("circulator: hold is not for here..");
1531 $self->remote_hold($hold);
1536 sub checkin_handle_precat {
1538 my $copy = $self->copy;
1540 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1541 $copy->status(OILS_COPY_STATUS_CATALOGING);
1542 $self->update_copy();
1543 $self->checkin_changed(1);
1544 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1549 sub checkin_build_copy_transit {
1552 my $copy = $self->copy;
1553 my $transit = Fieldmapper::action::transit_copy->new;
1555 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1556 $logger->info("circulator: transiting copy to $dest");
1558 $transit->source($self->editor->requestor->ws_ou);
1559 $transit->dest($dest);
1560 $transit->target_copy($copy->id);
1561 $transit->source_send_time('now');
1562 $transit->copy_status( $U->copy_status($copy->status)->id );
1564 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1566 return $self->bail_on_events($self->editor->event)
1567 unless $self->editor->create_action_transit_copy($transit);
1569 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1571 $self->checkin_changed(1);
1575 sub attempt_checkin_hold_capture {
1577 my $copy = $self->copy;
1579 # See if this copy can fulfill any holds
1580 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1581 $self->editor, $copy, $self->editor->requestor );
1584 $logger->debug("circulator: no potential permitted".
1585 "holds found for copy ".$copy->barcode);
1589 $self->retarget($retarget);
1591 $logger->info("circulator: found permitted hold ".
1592 $hold->id . " for copy, capturing...");
1594 $hold->current_copy($copy->id);
1595 $hold->capture_time('now');
1597 # prevent DB errors caused by fetching
1598 # holds from storage, and updating through cstore
1599 $hold->clear_fulfillment_time;
1600 $hold->clear_fulfillment_staff;
1601 $hold->clear_fulfillment_lib;
1602 $hold->clear_expire_time;
1603 $hold->clear_cancel_time;
1604 $hold->clear_prev_check_time unless $hold->prev_check_time;
1606 $self->bail_on_events($self->editor->event)
1607 unless $self->editor->update_action_hold_request($hold);
1609 $self->checkin_changed(1);
1611 return 1 if $self->bail_out;
1613 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1615 # This hold was captured in the correct location
1616 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1617 $self->push_events(OpenILS::Event->new('SUCCESS'));
1619 #$self->do_hold_notify($hold->id);
1620 $self->notify_hold($hold->id);
1624 # Hold needs to be picked up elsewhere. Build a hold
1625 # transit and route the item.
1626 $self->checkin_build_hold_transit();
1627 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1628 return 1 if $self->bail_out;
1630 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1633 # make sure we save the copy status
1638 sub do_hold_notify {
1639 my( $self, $holdid ) = @_;
1641 $logger->info("circulator: running delayed hold notify process");
1643 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1644 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1646 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1647 hold_id => $holdid, requestor => $self->editor->requestor);
1649 $logger->debug("circulator: built hold notifier");
1651 if(!$notifier->event) {
1653 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1655 my $stat = $notifier->send_email_notify;
1656 if( $stat == '1' ) {
1657 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1661 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1664 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1668 sub retarget_holds {
1669 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1670 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1671 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1672 # no reason to wait for the return value
1676 sub checkin_build_hold_transit {
1679 my $copy = $self->copy;
1680 my $hold = $self->hold;
1681 my $trans = Fieldmapper::action::hold_transit_copy->new;
1683 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1685 $trans->hold($hold->id);
1686 $trans->source($self->editor->requestor->ws_ou);
1687 $trans->dest($hold->pickup_lib);
1688 $trans->source_send_time("now");
1689 $trans->target_copy($copy->id);
1691 # when the copy gets to its destination, it will recover
1692 # this status - put it onto the holds shelf
1693 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1695 return $self->bail_on_events($self->editor->event)
1696 unless $self->editor->create_action_hold_transit_copy($trans);
1701 sub process_received_transit {
1703 my $copy = $self->copy;
1704 my $copyid = $self->copy->id;
1706 my $status_name = $U->copy_status($copy->status)->name;
1707 $logger->debug("circulator: attempting transit receive on ".
1708 "copy $copyid. Copy status is $status_name");
1710 my $transit = $self->transit;
1712 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1713 # - this item is in-transit to a different location
1715 my $tid = $transit->id;
1716 my $loc = $self->editor->requestor->ws_ou;
1717 my $dest = $transit->dest;
1719 $logger->info("circulator: Fowarding transit on copy which is destined ".
1720 "for a different location. transit=$tid, copy=$copyid, current ".
1721 "location=$loc, destination location=$dest");
1723 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1725 # grab the associated hold object if available
1726 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1727 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1729 return $self->bail_on_events($evt);
1732 # The transit is received, set the receive time
1733 $transit->dest_recv_time('now');
1734 $self->bail_on_events($self->editor->event)
1735 unless $self->editor->update_action_transit_copy($transit);
1737 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1739 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1740 $copy->status( $transit->copy_status );
1741 $self->update_copy();
1742 return if $self->bail_out;
1746 #$self->do_hold_notify($hold_transit->hold);
1747 $self->notify_hold($hold_transit->hold);
1752 OpenILS::Event->new(
1755 payload => { transit => $transit, holdtransit => $hold_transit } ));
1757 return $hold_transit;
1761 sub checkin_handle_circ {
1765 my $circ = $self->circ;
1766 my $copy = $self->copy;
1770 # backdate the circ if necessary
1771 if($self->backdate) {
1772 $self->checkin_handle_backdate;
1773 return if $self->bail_out;
1776 if(!$circ->stop_fines) {
1777 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1778 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1779 $circ->stop_fines_time('now') unless $self->backdate;
1780 $circ->stop_fines_time($self->backdate) if $self->backdate;
1783 # see if there are any fines owed on this circ. if not, close it
1784 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1785 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1787 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1789 # Set the checkin vars since we have the item
1790 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1792 $circ->checkin_staff($self->editor->requestor->id);
1793 $circ->checkin_lib($self->editor->requestor->ws_ou);
1795 my $circ_lib = (ref $self->copy->circ_lib) ?
1796 $self->copy->circ_lib->id : $self->copy->circ_lib;
1797 my $stat = $U->copy_status($self->copy->status)->id;
1799 # If the item is lost/missing and it needs to be sent home, don't
1800 # reshelve the copy, leave it lost/missing so the recipient will know
1801 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1802 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1803 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1806 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1810 return $self->bail_on_events($self->editor->event)
1811 unless $self->editor->update_action_circulation($circ);
1815 sub checkin_handle_backdate {
1818 my $bd = $self->backdate;
1820 # ------------------------------------------------------------------
1821 # clean up the backdate for date comparison
1822 # we want any bills created on or after the backdate
1823 # ------------------------------------------------------------------
1824 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1825 #$bd = "${bd}T23:59:59";
1827 my $bills = $self->editor->search_money_billing(
1829 billing_ts => { '>=' => $bd },
1830 xact => $self->circ->id,
1831 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1835 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1837 for my $bill (@$bills) {
1838 unless( $U->is_true($bill->voided) ) {
1839 $logger->info("backdate voiding bill ".$bill->id);
1841 $bill->void_time('now');
1842 $bill->voider($self->editor->requestor->id);
1843 my $n = $bill->note || "";
1844 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1846 $self->bail_on_events($self->editor->event)
1847 unless $self->editor->update_money_billing($bill);
1855 # XXX Legacy version for Circ.pm support
1856 sub _checkin_handle_backdate {
1857 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1860 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1861 $bd = "${bd}T23:59:59";
1863 my $bills = $session->request(
1864 "open-ils.storage.direct.money.billing.search_where.atomic",
1865 billing_ts => { '>=' => $bd },
1867 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1870 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1873 for my $bill (@$bills) {
1874 unless( $U->is_true($bill->voided) ) {
1875 $logger->debug("voiding bill ".$bill->id);
1877 $bill->void_time('now');
1878 $bill->voider($requestor->id);
1879 my $n = $bill->note || "";
1880 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1881 my $s = $session->request(
1882 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1883 return $U->DB_UPDATE_FAILED($bill) unless $s;
1897 sub find_patron_from_copy {
1899 my $circs = $self->editor->search_action_circulation(
1900 { target_copy => $self->copy->id, checkin_time => undef });
1901 my $circ = $circs->[0];
1902 return unless $circ;
1903 my $u = $self->editor->retrieve_actor_user($circ->usr)
1904 or return $self->bail_on_events($self->editor->event);
1908 sub check_checkin_copy_status {
1910 my $copy = $self->copy;
1916 my $status = $U->copy_status($copy->status)->id;
1919 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1920 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1921 $status == OILS_COPY_STATUS_IN_PROCESS ||
1922 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1923 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1924 $status == OILS_COPY_STATUS_CATALOGING ||
1925 $status == OILS_COPY_STATUS_RESHELVING );
1927 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1928 if( $status == OILS_COPY_STATUS_LOST );
1930 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1931 if( $status == OILS_COPY_STATUS_MISSING );
1933 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1938 # --------------------------------------------------------------------------
1939 # On checkin, we need to return as many relevant objects as we can
1940 # --------------------------------------------------------------------------
1941 sub checkin_flesh_events {
1944 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1945 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1946 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1950 for my $evt (@{$self->events}) {
1953 $payload->{copy} = $U->unflesh_copy($self->copy);
1954 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1955 $payload->{circ} = $self->circ;
1956 $payload->{transit} = $self->transit;
1957 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1959 # $self->hold may or may not have been replaced with a
1960 # valid hold after processing a cancelled hold
1961 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1963 $evt->{payload} = $payload;
1968 my( $self, $msg ) = @_;
1969 my $bc = ($self->copy) ? $self->copy->barcode :
1972 my $usr = ($self->patron) ? $self->patron->id : "";
1973 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1974 ", recipient=$usr, copy=$bc");
1980 $self->log_me("do_renew()");
1981 $self->is_renewal(1);
1983 # Make sure there is an open circ to renew that is not
1984 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1985 my $circ = $self->editor->search_action_circulation(
1986 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1989 $circ = $self->editor->search_action_circulation(
1991 target_copy => $self->copy->id,
1992 stop_fines => OILS_STOP_FINES_MAX_FINES,
1993 checkin_time => undef
1998 return $self->bail_on_events($self->editor->event) unless $circ;
2000 # A user is not allowed to renew another user's items without permission
2001 unless( $circ->usr eq $self->editor->requestor->id ) {
2002 return $self->bail_on_events($self->editor->events)
2003 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2006 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2007 if $circ->renewal_remaining < 1;
2009 # -----------------------------------------------------------------
2011 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2014 $self->run_renew_permit;
2017 $self->do_checkin();
2018 return if $self->bail_out;
2020 unless( $self->permit_override ) {
2022 return if $self->bail_out;
2023 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2024 $self->remove_event('ITEM_NOT_CATALOGED');
2027 $self->override_events;
2028 return if $self->bail_out;
2031 $self->do_checkout();
2036 my( $self, $evt ) = @_;
2037 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2038 $logger->debug("circulator: removing event from list: $evt");
2039 my @events = @{$self->events};
2040 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2045 my( $self, $evt ) = @_;
2046 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2047 return grep { $_->{textcode} eq $evt } @{$self->events};
2052 sub run_renew_permit {
2054 my $runner = $self->script_runner;
2056 $runner->load($self->circ_permit_renew);
2057 my $result = $runner->run or
2058 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2059 my $events = $result->{events};
2061 $logger->activity("ciculator: circ_permit_renew for user ".
2062 $self->patron->id." returned events: @$events") if @$events;
2064 $self->push_events(OpenILS::Event->new($_)) for @$events;
2066 $logger->debug("circulator: re-creating script runner to be safe");
2067 $self->mk_script_runner;