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";
17 my $conf = OpenSRF::Utils::SettingsClient->new;
18 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
19 my @pfx = ( @pfx2, "scripts" );
21 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
22 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
23 my $d = $conf->config_value( @pfx, 'circ_duration' );
24 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
25 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
26 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
27 my $lb = $conf->config_value( @pfx2, 'script_path' );
29 $logger->error( "Missing circ script(s)" )
30 unless( $p and $c and $d and $f and $m and $pr );
32 $scripts{circ_permit_patron} = $p;
33 $scripts{circ_permit_copy} = $c;
34 $scripts{circ_duration} = $d;
35 $scripts{circ_recurring_fines}= $f;
36 $scripts{circ_max_fines} = $m;
37 $scripts{circ_permit_renew} = $pr;
39 $lb = [ $lb ] unless ref($lb);
43 "circulator: Loaded rules scripts for circ: " .
44 "circ permit patron = $p, ".
45 "circ permit copy = $c, ".
46 "circ duration = $d, ".
47 "circ recurring fines = $f, " .
48 "circ max fines = $m, ".
49 "circ renew permit = $pr. ".
54 __PACKAGE__->register_method(
55 method => "run_method",
56 api_name => "open-ils.circ.checkout.permit",
58 Determines if the given checkout can occur
59 @param authtoken The login session key
60 @param params A trailing hash of named params including
61 barcode : The copy barcode,
62 patron : The patron the checkout is occurring for,
63 renew : true or false - whether or not this is a renewal
64 @return The event that occurred during the permit check.
68 __PACKAGE__->register_method (
69 method => 'run_method',
70 api_name => 'open-ils.circ.checkout.permit.override',
71 signature => q/@see open-ils.circ.checkout.permit/,
75 __PACKAGE__->register_method(
76 method => "run_method",
77 api_name => "open-ils.circ.checkout",
80 @param authtoken The login session key
81 @param params A named hash of params including:
83 barcode If no copy is provided, the copy is retrieved via barcode
84 copyid If no copy or barcode is provide, the copy id will be use
85 patron The patron's id
86 noncat True if this is a circulation for a non-cataloted item
87 noncat_type The non-cataloged type id
88 noncat_circ_lib The location for the noncat circ.
89 precat The item has yet to be cataloged
90 dummy_title The temporary title of the pre-cataloded item
91 dummy_author The temporary authr of the pre-cataloded item
92 Default is the home org of the staff member
93 @return The SUCCESS event on success, any other event depending on the error
96 __PACKAGE__->register_method(
97 method => "run_method",
98 api_name => "open-ils.circ.checkin",
101 Generic super-method for handling all copies
102 @param authtoken The login session key
103 @param params Hash of named parameters including:
104 barcode - The copy barcode
105 force - If true, copies in bad statuses will be checked in and give good statuses
110 __PACKAGE__->register_method(
111 method => "run_method",
112 api_name => "open-ils.circ.checkin.override",
113 signature => q/@see open-ils.circ.checkin/
116 __PACKAGE__->register_method(
117 method => "run_method",
118 api_name => "open-ils.circ.renew.override",
119 signature => q/@see open-ils.circ.renew/,
123 __PACKAGE__->register_method(
124 method => "run_method",
125 api_name => "open-ils.circ.renew",
126 notes => <<" NOTES");
127 PARAMS( authtoken, circ => circ_id );
128 open-ils.circ.renew(login_session, circ_object);
129 Renews the provided circulation. login_session is the requestor of the
130 renewal and if the logged in user is not the same as circ->usr, then
131 the logged in user must have RENEW_CIRC permissions.
134 __PACKAGE__->register_method(
135 method => "run_method",
136 api_name => "open-ils.circ.checkout.full");
137 __PACKAGE__->register_method(
138 method => "run_method",
139 api_name => "open-ils.circ.checkout.full.override");
144 my( $self, $conn, $auth, $args ) = @_;
145 translate_legacy_args($args);
146 my $api = $self->api_name;
149 OpenILS::Application::Circ::Circulator->new($auth, %$args);
151 return circ_events($circulator) if $circulator->bail_out;
153 # --------------------------------------------------------------------------
154 # Go ahead and load the script runner to make sure we have all
155 # of the objects we need
156 # --------------------------------------------------------------------------
157 $circulator->is_renewal(1) if $api =~ /renew/;
158 $circulator->is_checkin(1) if $api =~ /checkin/;
159 $circulator->check_penalty_on_renew(1) if
160 $circulator->is_renewal and $U->ou_ancestor_setting_value(
161 $circulator->editor->requestor->ws_ou, 'circ.renew.check_penalty', $circulator->editor);
162 $circulator->mk_script_runner;
163 return circ_events($circulator) if $circulator->bail_out;
165 $circulator->circ_permit_patron($scripts{circ_permit_patron});
166 $circulator->circ_permit_copy($scripts{circ_permit_copy});
167 $circulator->circ_duration($scripts{circ_duration});
168 $circulator->circ_permit_renew($scripts{circ_permit_renew});
170 $circulator->override(1) if $api =~ /override/o;
172 if( $api =~ /checkout\.permit/ ) {
173 $circulator->do_permit();
175 } elsif( $api =~ /checkout.full/ ) {
177 $circulator->do_permit();
178 unless( $circulator->bail_out ) {
179 $circulator->events([]);
180 $circulator->do_checkout();
183 } elsif( $api =~ /checkout/ ) {
184 $circulator->do_checkout();
186 } elsif( $api =~ /checkin/ ) {
187 $circulator->do_checkin();
189 } elsif( $api =~ /renew/ ) {
190 $circulator->is_renewal(1);
191 $circulator->do_renew();
194 if( $circulator->bail_out ) {
197 # make sure no success event accidentally slip in
199 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
202 my @e = @{$circulator->events};
203 push( @ee, $_->{textcode} ) for @e;
204 $logger->info("circulator: bailing out with events: @ee");
206 $circulator->editor->rollback;
209 $circulator->editor->commit;
212 $circulator->script_runner->cleanup;
214 $conn->respond_complete(circ_events($circulator));
216 unless($circulator->bail_out) {
217 $circulator->do_hold_notify($circulator->notify_hold)
218 if $circulator->notify_hold;
219 $circulator->retarget_holds if $circulator->retarget;
225 my @e = @{$circ->events};
226 # if we have multiple events, SUCCESS should not be one of them;
227 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
228 return (@e == 1) ? $e[0] : \@e;
233 sub translate_legacy_args {
236 if( $$args{barcode} ) {
237 $$args{copy_barcode} = $$args{barcode};
238 delete $$args{barcode};
241 if( $$args{copyid} ) {
242 $$args{copy_id} = $$args{copyid};
243 delete $$args{copyid};
246 if( $$args{patronid} ) {
247 $$args{patron_id} = $$args{patronid};
248 delete $$args{patronid};
251 if( $$args{patron} and !ref($$args{patron}) ) {
252 $$args{patron_id} = $$args{patron};
253 delete $$args{patron};
257 if( $$args{noncat} ) {
258 $$args{is_noncat} = $$args{noncat};
259 delete $$args{noncat};
262 if( $$args{precat} ) {
263 $$args{is_precat} = $$args{precat};
264 delete $$args{precat};
270 # --------------------------------------------------------------------------
271 # This package actually manages all of the circulation logic
272 # --------------------------------------------------------------------------
273 package OpenILS::Application::Circ::Circulator;
274 use strict; use warnings;
275 use vars q/$AUTOLOAD/;
277 use OpenILS::Utils::Fieldmapper;
278 use OpenSRF::Utils::Cache;
279 use Digest::MD5 qw(md5_hex);
280 use DateTime::Format::ISO8601;
281 use OpenILS::Utils::PermitHold;
282 use OpenSRF::Utils qw/:datetime/;
283 use OpenSRF::Utils::SettingsClient;
284 use OpenILS::Application::Circ::Holds;
285 use OpenILS::Application::Circ::Transit;
286 use OpenSRF::Utils::Logger qw(:logger);
287 use OpenILS::Utils::CStoreEditor qw/:funcs/;
288 use OpenILS::Application::Circ::ScriptBuilder;
289 use OpenILS::Const qw/:const/;
291 my $holdcode = "OpenILS::Application::Circ::Holds";
292 my $transcode = "OpenILS::Application::Circ::Transit";
297 # --------------------------------------------------------------------------
298 # Add a pile of automagic getter/setter methods
299 # --------------------------------------------------------------------------
300 my @AUTOLOAD_FIELDS = qw/
315 check_penalty_on_renew
342 recurring_fines_level
355 cancelled_hold_transit
365 my $type = ref($self) or die "$self is not an object";
367 my $name = $AUTOLOAD;
370 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
371 $logger->error("circulator: $type: invalid autoload field: $name");
372 die "$type: invalid autoload field: $name\n"
377 *{"${type}::${name}"} = sub {
380 $s->{$name} = $v if defined $v;
384 return $self->$name($data);
389 my( $class, $auth, %args ) = @_;
390 $class = ref($class) || $class;
391 my $self = bless( {}, $class );
395 new_editor(xact => 1, authtoken => $auth) );
397 unless( $self->editor->checkauth ) {
398 $self->bail_on_events($self->editor->event);
402 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
404 $self->$_($args{$_}) for keys %args;
407 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
409 # if this is a renewal, default to desk_renewal
410 $self->desk_renewal(1) unless
411 $self->opac_renewal or $self->phone_renewal;
417 # --------------------------------------------------------------------------
418 # True if we should discontinue processing
419 # --------------------------------------------------------------------------
421 my( $self, $bool ) = @_;
422 if( defined $bool ) {
423 $logger->info("circulator: BAILING OUT") if $bool;
424 $self->{bail_out} = $bool;
426 return $self->{bail_out};
431 my( $self, @evts ) = @_;
434 $logger->info("circulator: pushing event ".$e->{textcode});
435 push( @{$self->events}, $e ) unless
436 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
442 my $key = md5_hex( time() . rand() . "$$" );
443 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
444 return $self->permit_key($key);
447 sub check_permit_key {
449 my $key = $self->permit_key;
450 return 0 unless $key;
451 my $k = "oils_permit_key_$key";
452 my $one = $self->cache_handle->get_cache($k);
453 $self->cache_handle->delete_cache($k);
454 return ($one) ? 1 : 0;
458 # --------------------------------------------------------------------------
459 # This builds the script runner environment and fetches most of the
461 # --------------------------------------------------------------------------
462 sub mk_script_runner {
468 qw/copy copy_barcode copy_id patron
469 patron_id patron_barcode volume title editor/;
471 # Translate our objects into the ScriptBuilder args hash
472 $$args{$_} = $self->$_() for @fields;
474 $args->{ignore_user_status} = 1 if $self->is_checkin;
475 $$args{fetch_patron_by_circ_copy} = 1;
476 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
478 if( my $pco = $self->pending_checkouts ) {
479 $logger->info("circulator: we were given a pending checkouts number of $pco");
480 $$args{patronItemsOut} = $pco;
483 # This fetches most of the objects we need
484 $self->script_runner(
485 OpenILS::Application::Circ::ScriptBuilder->build($args));
487 # Now we translate the ScriptBuilder objects back into self
488 $self->$_($$args{$_}) for @fields;
490 my @evts = @{$args->{_events}} if $args->{_events};
492 $logger->debug("circulator: script builder returned events: @evts") if @evts;
496 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
497 if(!$self->is_noncat and
499 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
503 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
504 return $self->bail_on_events(@e);
508 $self->is_precat(1) if $self->copy
509 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
511 # We can't renew if there is no copy
512 return $self->bail_on_events(@evts) if
513 $self->is_renewal and !$self->copy;
515 # Set some circ-specific flags in the script environment
516 my $evt = "environment";
517 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
519 if( $self->is_noncat ) {
520 $self->script_runner->insert("$evt.isNonCat", 1);
521 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
524 if( $self->is_precat ) {
525 $self->script_runner->insert("environment.isPrecat", 1, 1);
528 $self->script_runner->add_path( $_ ) for @$script_libs;
536 # --------------------------------------------------------------------------
537 # Does the circ permit work
538 # --------------------------------------------------------------------------
542 $self->log_me("do_permit()");
544 unless( $self->editor->requestor->id == $self->patron->id ) {
545 return $self->bail_on_events($self->editor->event)
546 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
549 $self->check_captured_holds();
550 $self->do_copy_checks();
551 return if $self->bail_out;
552 $self->run_patron_permit_scripts();
553 $self->run_copy_permit_scripts()
554 unless $self->is_precat or $self->is_noncat;
555 $self->override_events() unless
556 $self->is_renewal and not $self->check_penalty_on_renew;
557 return if $self->bail_out;
559 if( $self->is_precat ) {
562 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
563 return $self->bail_out(1) unless $self->is_renewal;
569 payload => $self->mk_permit_key));
573 sub check_captured_holds {
575 my $copy = $self->copy;
576 my $patron = $self->patron;
578 return undef unless $copy;
580 my $s = $U->copy_status($copy->status)->id;
581 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
582 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
584 # Item is on the holds shelf, make sure it's going to the right person
585 my $holds = $self->editor->search_action_hold_request(
588 current_copy => $copy->id ,
589 capture_time => { '!=' => undef },
590 cancel_time => undef,
591 fulfillment_time => undef
597 if( $holds and $$holds[0] ) {
598 return undef if $$holds[0]->usr == $patron->id;
601 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
603 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
609 my $copy = $self->copy;
612 my $stat = $U->copy_status($copy->status)->id;
614 # We cannot check out a copy if it is in-transit
615 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
616 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
619 $self->handle_claims_returned();
620 return if $self->bail_out;
622 # no claims returned circ was found, check if there is any open circ
623 unless( $self->is_renewal ) {
624 my $circs = $self->editor->search_action_circulation(
625 { target_copy => $copy->id, checkin_time => undef }
628 return $self->bail_on_events(
629 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
634 sub send_penalty_request {
636 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
637 $self->penalty_request(
639 'open-ils.penalty.patron_penalty.calculate',
641 authtoken => $self->editor->authtoken,
642 patron => $self->patron } ) );
645 sub gather_penalty_request {
647 return [] unless $self->penalty_request;
648 my $data = $self->penalty_request->recv;
650 throw $data if UNIVERSAL::isa($data,'Error');
651 $data = $data->content;
652 return $data->{fatal_penalties};
654 $logger->error("circulator: penalty request returned no data");
658 # ---------------------------------------------------------------------
659 # This pushes any patron-related events into the list but does not
660 # set bail_out for any events
661 # ---------------------------------------------------------------------
662 sub run_patron_permit_scripts {
664 my $runner = $self->script_runner;
665 my $patronid = $self->patron->id;
667 $self->send_penalty_request() unless
668 $self->is_renewal and not $self->check_penalty_on_renew;
671 # ---------------------------------------------------------------------
672 # Now run the patron permit script
673 # ---------------------------------------------------------------------
674 $runner->load($self->circ_permit_patron);
675 my $result = $runner->run or
676 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
678 my $patron_events = $result->{events};
681 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
682 [] : $self->gather_penalty_request();
684 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
686 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
688 $self->push_events(@allevents);
692 sub run_copy_permit_scripts {
694 my $copy = $self->copy || return;
695 my $runner = $self->script_runner;
697 # ---------------------------------------------------------------------
698 # Capture all of the copy permit events
699 # ---------------------------------------------------------------------
700 $runner->load($self->circ_permit_copy);
701 my $result = $runner->run or
702 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
703 my $copy_events = $result->{events};
705 # ---------------------------------------------------------------------
706 # Now collect all of the events together
707 # ---------------------------------------------------------------------
709 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
711 # See if this copy has an alert message
712 my $ae = $self->check_copy_alert();
713 push( @allevents, $ae ) if $ae;
715 # uniquify the events
716 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
717 @allevents = values %hash;
720 $_->{payload} = $copy if
721 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
724 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
726 $self->push_events(@allevents);
730 sub check_copy_alert {
732 return undef if $self->is_renewal;
733 return OpenILS::Event->new(
734 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
735 if $self->copy and $self->copy->alert_message;
741 # --------------------------------------------------------------------------
742 # If the call is overriding and has permissions to override every collected
743 # event, the are cleared. Any event that the caller does not have
744 # permission to override, will be left in the event list and bail_out will
746 # XXX We need code in here to cancel any holds/transits on copies
747 # that are being force-checked out
748 # --------------------------------------------------------------------------
749 sub override_events {
751 my @events = @{$self->events};
752 return unless @events;
754 if(!$self->override) {
755 return $self->bail_out(1)
756 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
761 for my $e (@events) {
762 my $tc = $e->{textcode};
763 next if $tc eq 'SUCCESS';
764 my $ov = "$tc.override";
765 $logger->info("circulator: attempting to override event: $ov");
767 return $self->bail_on_events($self->editor->event)
768 unless( $self->editor->allowed($ov) );
773 # --------------------------------------------------------------------------
774 # If there is an open claimsreturn circ on the requested copy, close the
775 # circ if overriding, otherwise bail out
776 # --------------------------------------------------------------------------
777 sub handle_claims_returned {
779 my $copy = $self->copy;
781 my $CR = $self->editor->search_action_circulation(
783 target_copy => $copy->id,
784 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
785 checkin_time => undef,
789 return unless ($CR = $CR->[0]);
793 # - If the caller has set the override flag, we will check the item in
794 if($self->override) {
796 $CR->checkin_time('now');
797 $CR->checkin_lib($self->editor->requestor->ws_ou);
798 $CR->checkin_staff($self->editor->requestor->id);
800 $evt = $self->editor->event
801 unless $self->editor->update_action_circulation($CR);
804 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
807 $self->bail_on_events($evt) if $evt;
812 # --------------------------------------------------------------------------
813 # This performs the checkout
814 # --------------------------------------------------------------------------
818 $self->log_me("do_checkout()");
820 # make sure perms are good if this isn't a renewal
821 unless( $self->is_renewal ) {
822 return $self->bail_on_events($self->editor->event)
823 unless( $self->editor->allowed('COPY_CHECKOUT') );
826 # verify the permit key
827 unless( $self->check_permit_key ) {
828 if( $self->permit_override ) {
829 return $self->bail_on_events($self->editor->event)
830 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
832 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
836 # if this is a non-cataloged circ, build the circ and finish
837 if( $self->is_noncat ) {
838 $self->checkout_noncat;
840 OpenILS::Event->new('SUCCESS',
841 payload => { noncat_circ => $self->circ }));
845 if( $self->is_precat ) {
846 $self->script_runner->insert("environment.isPrecat", 1, 1);
847 $self->make_precat_copy;
848 return if $self->bail_out;
850 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
851 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
854 $self->do_copy_checks;
855 return if $self->bail_out;
857 $self->run_checkout_scripts();
858 return if $self->bail_out;
860 $self->build_checkout_circ_object();
861 return if $self->bail_out;
863 $self->apply_modified_due_date();
864 return if $self->bail_out;
866 return $self->bail_on_events($self->editor->event)
867 unless $self->editor->create_action_circulation($self->circ);
869 # refresh the circ to force local time zone for now
870 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
872 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
874 return if $self->bail_out;
876 $self->handle_checkout_holds();
877 return if $self->bail_out;
879 # ------------------------------------------------------------------------------
880 # Update the patron penalty info in the DB. Run it for permit-overrides or
881 # renewals since both of those cases do not require the penalty server to
882 # run during the permit phase of the checkout
883 # ------------------------------------------------------------------------------
884 if( $self->permit_override or $self->is_renewal ) {
885 $U->update_patron_penalties(
886 authtoken => $self->editor->authtoken,
887 patron => $self->patron,
892 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
894 OpenILS::Event->new('SUCCESS',
896 copy => $U->unflesh_copy($self->copy),
899 holds_fulfilled => $self->fulfilled_holds,
907 my $copy = $self->copy;
909 my $stat = $copy->status if ref $copy->status;
910 my $loc = $copy->location if ref $copy->location;
911 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
913 $copy->status($stat->id) if $stat;
914 $copy->location($loc->id) if $loc;
915 $copy->circ_lib($circ_lib->id) if $circ_lib;
916 $copy->editor($self->editor->requestor->id);
917 $copy->edit_date('now');
918 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
920 return $self->bail_on_events($self->editor->event)
921 unless $self->editor->update_asset_copy($self->copy);
923 $copy->status($U->copy_status($copy->status));
924 $copy->location($loc) if $loc;
925 $copy->circ_lib($circ_lib) if $circ_lib;
930 my( $self, @evts ) = @_;
931 $self->push_events(@evts);
935 sub handle_checkout_holds {
938 my $copy = $self->copy;
939 my $patron = $self->patron;
941 my $holds = $self->editor->search_action_hold_request(
943 current_copy => $copy->id ,
944 cancel_time => undef,
945 fulfillment_time => undef
951 # XXX We should only fulfill one hold here...
952 # XXX If a hold was transited to the user who is checking out
953 # the item, we need to make sure that hold is what's grabbed
956 # for now, just sort by id to get what should be the oldest hold
957 $holds = [ sort { $a->id <=> $b->id } @$holds ];
958 my @myholds = grep { $_->usr eq $patron->id } @$holds;
959 my @altholds = grep { $_->usr ne $patron->id } @$holds;
962 my $hold = $myholds[0];
964 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
966 # if the hold was never officially captured, capture it.
967 $hold->capture_time('now') unless $hold->capture_time;
969 # just make sure it's set correctly
970 $hold->current_copy($copy->id);
972 $hold->fulfillment_time('now');
973 $hold->fulfillment_staff($self->editor->requestor->id);
974 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
976 return $self->bail_on_events($self->editor->event)
977 unless $self->editor->update_action_hold_request($hold);
979 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
981 push( @fulfilled, $hold->id );
984 # If there are any holds placed for other users that point to this copy,
985 # then we need to un-target those holds so the targeter can pick a new copy
988 $logger->info("circulator: un-targeting hold ".$_->id.
989 " because copy ".$copy->id." is getting checked out");
991 # - make the targeter process this hold at next run
992 $_->clear_prev_check_time;
994 # - clear out the targetted copy
995 $_->clear_current_copy;
996 $_->clear_capture_time;
998 return $self->bail_on_event($self->editor->event)
999 unless $self->editor->update_action_hold_request($_);
1003 $self->fulfilled_holds(\@fulfilled);
1008 sub run_checkout_scripts {
1012 my $runner = $self->script_runner;
1013 $runner->load($self->circ_duration);
1015 my $result = $runner->run or
1016 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1018 my $duration = $result->{durationRule};
1019 my $recurring = $result->{recurringFinesRule};
1020 my $max_fine = $result->{maxFine};
1022 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1024 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1025 return $self->bail_on_events($evt) if $evt;
1027 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1028 return $self->bail_on_events($evt) if $evt;
1030 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1031 return $self->bail_on_events($evt) if $evt;
1035 # The item circulates with an unlimited duration
1041 $self->duration_rule($duration);
1042 $self->recurring_fines_rule($recurring);
1043 $self->max_fine_rule($max_fine);
1047 sub build_checkout_circ_object {
1050 my $circ = Fieldmapper::action::circulation->new;
1051 my $duration = $self->duration_rule;
1052 my $max = $self->max_fine_rule;
1053 my $recurring = $self->recurring_fines_rule;
1054 my $copy = $self->copy;
1055 my $patron = $self->patron;
1059 my $dname = $duration->name;
1060 my $mname = $max->name;
1061 my $rname = $recurring->name;
1063 $logger->debug("circulator: building circulation ".
1064 "with duration=$dname, maxfine=$mname, recurring=$rname");
1066 $circ->duration( $duration->shrt )
1067 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1068 $circ->duration( $duration->normal )
1069 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1070 $circ->duration( $duration->extended )
1071 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1073 $circ->recuring_fine( $recurring->low )
1074 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1075 $circ->recuring_fine( $recurring->normal )
1076 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1077 $circ->recuring_fine( $recurring->high )
1078 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1080 $circ->duration_rule( $duration->name );
1081 $circ->recuring_fine_rule( $recurring->name );
1082 $circ->max_fine_rule( $max->name );
1083 $circ->max_fine( $max->amount );
1085 $circ->fine_interval($recurring->recurance_interval);
1086 $circ->renewal_remaining( $duration->max_renewals );
1090 $logger->info("circulator: copy found with an unlimited circ duration");
1091 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1092 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1093 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1094 $circ->renewal_remaining(0);
1097 $circ->target_copy( $copy->id );
1098 $circ->usr( $patron->id );
1099 $circ->circ_lib( $self->circ_lib );
1101 if( $self->is_renewal ) {
1102 $circ->opac_renewal('t') if $self->opac_renewal;
1103 $circ->phone_renewal('t') if $self->phone_renewal;
1104 $circ->desk_renewal('t') if $self->desk_renewal;
1105 $circ->renewal_remaining($self->renewal_remaining);
1106 $circ->circ_staff($self->editor->requestor->id);
1110 # if the user provided an overiding checkout time,
1111 # (e.g. the checkout really happened several hours ago), then
1112 # we apply that here. Does this need a perm??
1113 $circ->xact_start(clense_ISO8601($self->checkout_time))
1114 if $self->checkout_time;
1116 # if a patron is renewing, 'requestor' will be the patron
1117 $circ->circ_staff($self->editor->requestor->id);
1118 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1124 sub apply_modified_due_date {
1126 my $circ = $self->circ;
1127 my $copy = $self->copy;
1129 if( $self->due_date ) {
1131 return $self->bail_on_events($self->editor->event)
1132 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1134 $circ->due_date(clense_ISO8601($self->due_date));
1138 # if the due_date lands on a day when the location is closed
1139 return unless $copy and $circ->due_date;
1141 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1143 # due-date overlap should be determined by the location the item
1144 # is checked out from, not the owning or circ lib of the item
1145 my $org = $self->editor->requestor->ws_ou;
1147 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1148 " with an item due date of ".$circ->due_date );
1150 my $dateinfo = $U->storagereq(
1151 'open-ils.storage.actor.org_unit.closed_date.overlap',
1152 $org, $circ->due_date );
1155 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1156 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1158 # XXX make the behavior more dynamic
1159 # for now, we just push the due date to after the close date
1160 $circ->due_date($dateinfo->{end});
1167 sub create_due_date {
1168 my( $self, $duration ) = @_;
1169 # if there is a raw time component (e.g. from postgres),
1170 # turn it into an interval that interval_to_seconds can parse
1171 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1172 my ($sec,$min,$hour,$mday,$mon,$year) =
1173 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1174 $year += 1900; $mon += 1;
1175 my $due_date = sprintf(
1176 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1177 $year, $mon, $mday, $hour, $min, $sec);
1183 sub make_precat_copy {
1185 my $copy = $self->copy;
1188 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1190 $copy->editor($self->editor->requestor->id);
1191 $copy->edit_date('now');
1192 $copy->dummy_title($self->dummy_title);
1193 $copy->dummy_author($self->dummy_author);
1195 $self->update_copy();
1199 $logger->info("circulator: Creating a new precataloged ".
1200 "copy in checkout with barcode " . $self->copy_barcode);
1202 $copy = Fieldmapper::asset::copy->new;
1203 $copy->circ_lib($self->circ_lib);
1204 $copy->creator($self->editor->requestor->id);
1205 $copy->editor($self->editor->requestor->id);
1206 $copy->barcode($self->copy_barcode);
1207 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1208 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1209 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1211 $copy->dummy_title($self->dummy_title || "");
1212 $copy->dummy_author($self->dummy_author || "");
1214 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1216 $self->push_events($self->editor->event);
1220 # this is a little bit of a hack, but we need to
1221 # get the copy into the script runner
1222 $self->script_runner->insert("environment.copy", $copy, 1);
1226 sub checkout_noncat {
1232 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1233 my $count = $self->noncat_count || 1;
1234 my $cotime = clense_ISO8601($self->checkout_time) || "";
1236 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1240 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1241 $self->editor->requestor->id,
1249 $self->push_events($evt);
1260 $self->log_me("do_checkin()");
1263 return $self->bail_on_events(
1264 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1267 if( $self->checkin_check_holds_shelf() ) {
1268 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1269 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1270 $self->checkin_flesh_events;
1274 unless( $self->is_renewal ) {
1275 return $self->bail_on_events($self->editor->event)
1276 unless $self->editor->allowed('COPY_CHECKIN');
1279 $self->push_events($self->check_copy_alert());
1280 $self->push_events($self->check_checkin_copy_status());
1282 # the renew code will have already found our circulation object
1283 unless( $self->is_renewal and $self->circ ) {
1284 my $circs = $self->editor->search_action_circulation(
1285 { target_copy => $self->copy->id, checkin_time => undef });
1286 $self->circ($$circs[0]);
1288 # for now, just warn if there are multiple open circs on a copy
1289 $logger->warn("circulator: we have ".scalar(@$circs).
1290 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1293 # if the circ is marked as 'claims returned', add the event to the list
1294 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1295 if ($self->circ and $self->circ->stop_fines
1296 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1298 # handle the overridable events
1299 $self->override_events unless $self->is_renewal;
1300 return if $self->bail_out;
1304 $self->editor->search_action_transit_copy(
1305 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1309 $self->checkin_handle_circ;
1310 return if $self->bail_out;
1311 $self->checkin_changed(1);
1313 } elsif( $self->transit ) {
1314 my $hold_transit = $self->process_received_transit;
1315 $self->checkin_changed(1);
1317 if( $self->bail_out ) {
1318 $self->checkin_flesh_events;
1322 if( my $e = $self->check_checkin_copy_status() ) {
1323 # If the original copy status is special, alert the caller
1324 my $ev = $self->events;
1325 $self->events([$e]);
1326 $self->override_events;
1327 return if $self->bail_out;
1331 if( $hold_transit or
1332 $U->copy_status($self->copy->status)->id
1333 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1336 if( $hold_transit ) {
1337 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1339 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1344 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1346 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1347 $self->reshelve_copy(1);
1348 $self->cancelled_hold_transit(1);
1349 $self->notify_hold(0); # don't notify for cancelled holds
1350 return if $self->bail_out;
1354 # hold transited to correct location
1355 $self->checkin_flesh_events;
1360 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1362 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1363 " that is in-transit, but there is no transit.. repairing");
1364 $self->reshelve_copy(1);
1365 return if $self->bail_out;
1368 if( $self->is_renewal ) {
1369 $self->push_events(OpenILS::Event->new('SUCCESS'));
1373 # ------------------------------------------------------------------------------
1374 # Circulations and transits are now closed where necessary. Now go on to see if
1375 # this copy can fulfill a hold or needs to be routed to a different location
1376 # ------------------------------------------------------------------------------
1378 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1379 return if $self->bail_out;
1381 } else { # not needed for a hold
1383 my $circ_lib = (ref $self->copy->circ_lib) ?
1384 $self->copy->circ_lib->id : $self->copy->circ_lib;
1386 if( $self->remote_hold ) {
1387 $circ_lib = $self->remote_hold->pickup_lib;
1388 $logger->warn("circulator: Copy ".$self->copy->barcode.
1389 " is on a remote hold's shelf, sending to $circ_lib");
1392 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1394 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1396 $self->checkin_handle_precat();
1397 return if $self->bail_out;
1401 my $bc = $self->copy->barcode;
1402 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1403 $self->checkin_build_copy_transit($circ_lib);
1404 return if $self->bail_out;
1405 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1409 $self->reshelve_copy;
1410 return if $self->bail_out;
1412 unless($self->checkin_changed) {
1414 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1415 my $stat = $U->copy_status($self->copy->status)->id;
1417 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1418 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1419 $self->bail_out(1); # no need to commit anything
1423 $self->push_events(OpenILS::Event->new('SUCCESS'))
1424 unless @{$self->events};
1428 # ------------------------------------------------------------------------------
1429 # Update the patron penalty info in the DB
1430 # ------------------------------------------------------------------------------
1431 $U->update_patron_penalties(
1432 authtoken => $self->editor->authtoken,
1433 patron => $self->patron,
1434 background => 1 ) if $self->is_checkin;
1436 $self->checkin_flesh_events;
1442 my $force = $self->force || shift;
1443 my $copy = $self->copy;
1445 my $stat = $U->copy_status($copy->status)->id;
1448 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1449 $stat != OILS_COPY_STATUS_CATALOGING and
1450 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1451 $stat != OILS_COPY_STATUS_RESHELVING )) {
1453 $copy->status( OILS_COPY_STATUS_RESHELVING );
1455 $self->checkin_changed(1);
1460 # Returns true if the item is at the current location
1461 # because it was transited there for a hold and the
1462 # hold has not been fulfilled
1463 sub checkin_check_holds_shelf {
1465 return 0 unless $self->copy;
1468 $U->copy_status($self->copy->status)->id ==
1469 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1471 # find the hold that put us on the holds shelf
1472 my $holds = $self->editor->search_action_hold_request(
1474 current_copy => $self->copy->id,
1475 capture_time => { '!=' => undef },
1476 fulfillment_time => undef,
1477 cancel_time => undef,
1482 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1483 $self->reshelve_copy(1);
1487 my $hold = $$holds[0];
1489 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1490 $hold->id. "] for copy ".$self->copy->barcode);
1492 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1493 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1497 $logger->info("circulator: hold is not for here..");
1498 $self->remote_hold($hold);
1503 sub checkin_handle_precat {
1505 my $copy = $self->copy;
1507 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1508 $copy->status(OILS_COPY_STATUS_CATALOGING);
1509 $self->update_copy();
1510 $self->checkin_changed(1);
1511 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1516 sub checkin_build_copy_transit {
1519 my $copy = $self->copy;
1520 my $transit = Fieldmapper::action::transit_copy->new;
1522 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1523 $logger->info("circulator: transiting copy to $dest");
1525 $transit->source($self->editor->requestor->ws_ou);
1526 $transit->dest($dest);
1527 $transit->target_copy($copy->id);
1528 $transit->source_send_time('now');
1529 $transit->copy_status( $U->copy_status($copy->status)->id );
1531 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1533 return $self->bail_on_events($self->editor->event)
1534 unless $self->editor->create_action_transit_copy($transit);
1536 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1538 $self->checkin_changed(1);
1542 sub attempt_checkin_hold_capture {
1544 my $copy = $self->copy;
1546 # See if this copy can fulfill any holds
1547 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1548 $self->editor, $copy, $self->editor->requestor );
1551 $logger->debug("circulator: no potential permitted".
1552 "holds found for copy ".$copy->barcode);
1556 $self->retarget($retarget);
1558 $logger->info("circulator: found permitted hold ".
1559 $hold->id . " for copy, capturing...");
1561 $hold->current_copy($copy->id);
1562 $hold->capture_time('now');
1564 # prevent DB errors caused by fetching
1565 # holds from storage, and updating through cstore
1566 $hold->clear_fulfillment_time;
1567 $hold->clear_fulfillment_staff;
1568 $hold->clear_fulfillment_lib;
1569 $hold->clear_expire_time;
1570 $hold->clear_cancel_time;
1571 $hold->clear_prev_check_time unless $hold->prev_check_time;
1573 $self->bail_on_events($self->editor->event)
1574 unless $self->editor->update_action_hold_request($hold);
1576 $self->checkin_changed(1);
1578 return 1 if $self->bail_out;
1580 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1582 # This hold was captured in the correct location
1583 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1584 $self->push_events(OpenILS::Event->new('SUCCESS'));
1586 #$self->do_hold_notify($hold->id);
1587 $self->notify_hold($hold->id);
1591 # Hold needs to be picked up elsewhere. Build a hold
1592 # transit and route the item.
1593 $self->checkin_build_hold_transit();
1594 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1595 return 1 if $self->bail_out;
1597 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1600 # make sure we save the copy status
1605 sub do_hold_notify {
1606 my( $self, $holdid ) = @_;
1608 $logger->info("circulator: running delayed hold notify process");
1610 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1611 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1613 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1614 hold_id => $holdid, requestor => $self->editor->requestor);
1616 $logger->debug("circulator: built hold notifier");
1618 if(!$notifier->event) {
1620 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1622 my $stat = $notifier->send_email_notify;
1623 if( $stat == '1' ) {
1624 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1628 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1631 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1635 sub retarget_holds {
1636 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1637 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1638 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1639 # no reason to wait for the return value
1643 sub checkin_build_hold_transit {
1646 my $copy = $self->copy;
1647 my $hold = $self->hold;
1648 my $trans = Fieldmapper::action::hold_transit_copy->new;
1650 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1652 $trans->hold($hold->id);
1653 $trans->source($self->editor->requestor->ws_ou);
1654 $trans->dest($hold->pickup_lib);
1655 $trans->source_send_time("now");
1656 $trans->target_copy($copy->id);
1658 # when the copy gets to its destination, it will recover
1659 # this status - put it onto the holds shelf
1660 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1662 return $self->bail_on_events($self->editor->event)
1663 unless $self->editor->create_action_hold_transit_copy($trans);
1668 sub process_received_transit {
1670 my $copy = $self->copy;
1671 my $copyid = $self->copy->id;
1673 my $status_name = $U->copy_status($copy->status)->name;
1674 $logger->debug("circulator: attempting transit receive on ".
1675 "copy $copyid. Copy status is $status_name");
1677 my $transit = $self->transit;
1679 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1680 # - this item is in-transit to a different location
1682 my $tid = $transit->id;
1683 my $loc = $self->editor->requestor->ws_ou;
1684 my $dest = $transit->dest;
1686 $logger->info("circulator: Fowarding transit on copy which is destined ".
1687 "for a different location. transit=$tid, copy=$copyid, current ".
1688 "location=$loc, destination location=$dest");
1690 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1692 # grab the associated hold object if available
1693 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1694 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1696 return $self->bail_on_events($evt);
1699 # The transit is received, set the receive time
1700 $transit->dest_recv_time('now');
1701 $self->bail_on_events($self->editor->event)
1702 unless $self->editor->update_action_transit_copy($transit);
1704 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1706 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1707 $copy->status( $transit->copy_status );
1708 $self->update_copy();
1709 return if $self->bail_out;
1713 #$self->do_hold_notify($hold_transit->hold);
1714 $self->notify_hold($hold_transit->hold);
1719 OpenILS::Event->new(
1722 payload => { transit => $transit, holdtransit => $hold_transit } ));
1724 return $hold_transit;
1728 sub checkin_handle_circ {
1732 my $circ = $self->circ;
1733 my $copy = $self->copy;
1737 # backdate the circ if necessary
1738 if($self->backdate) {
1739 $self->checkin_handle_backdate;
1740 return if $self->bail_out;
1743 if(!$circ->stop_fines) {
1744 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1745 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1746 $circ->stop_fines_time('now') unless $self->backdate;
1747 $circ->stop_fines_time($self->backdate) if $self->backdate;
1750 # see if there are any fines owed on this circ. if not, close it
1751 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1752 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1754 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1756 # Set the checkin vars since we have the item
1757 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1759 $circ->checkin_staff($self->editor->requestor->id);
1760 $circ->checkin_lib($self->editor->requestor->ws_ou);
1762 my $circ_lib = (ref $self->copy->circ_lib) ?
1763 $self->copy->circ_lib->id : $self->copy->circ_lib;
1764 my $stat = $U->copy_status($self->copy->status)->id;
1766 # If the item is lost/missing and it needs to be sent home, don't
1767 # reshelve the copy, leave it lost/missing so the recipient will know
1768 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1769 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1770 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1773 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1777 return $self->bail_on_events($self->editor->event)
1778 unless $self->editor->update_action_circulation($circ);
1782 sub checkin_handle_backdate {
1785 my $bd = $self->backdate;
1787 # ------------------------------------------------------------------
1788 # clean up the backdate for date comparison
1789 # we want any bills created on or after the backdate
1790 # ------------------------------------------------------------------
1791 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1792 #$bd = "${bd}T23:59:59";
1794 my $bills = $self->editor->search_money_billing(
1796 billing_ts => { '>=' => $bd },
1797 xact => $self->circ->id,
1798 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1802 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1804 for my $bill (@$bills) {
1805 unless( $U->is_true($bill->voided) ) {
1806 $logger->info("backdate voiding bill ".$bill->id);
1808 $bill->void_time('now');
1809 $bill->voider($self->editor->requestor->id);
1810 my $n = $bill->note || "";
1811 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1813 $self->bail_on_events($self->editor->event)
1814 unless $self->editor->update_money_billing($bill);
1822 # XXX Legacy version for Circ.pm support
1823 sub _checkin_handle_backdate {
1824 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1827 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1828 $bd = "${bd}T23:59:59";
1830 my $bills = $session->request(
1831 "open-ils.storage.direct.money.billing.search_where.atomic",
1832 billing_ts => { '>=' => $bd },
1834 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1837 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1840 for my $bill (@$bills) {
1841 unless( $U->is_true($bill->voided) ) {
1842 $logger->debug("voiding bill ".$bill->id);
1844 $bill->void_time('now');
1845 $bill->voider($requestor->id);
1846 my $n = $bill->note || "";
1847 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1848 my $s = $session->request(
1849 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1850 return $U->DB_UPDATE_FAILED($bill) unless $s;
1864 sub find_patron_from_copy {
1866 my $circs = $self->editor->search_action_circulation(
1867 { target_copy => $self->copy->id, checkin_time => undef });
1868 my $circ = $circs->[0];
1869 return unless $circ;
1870 my $u = $self->editor->retrieve_actor_user($circ->usr)
1871 or return $self->bail_on_events($self->editor->event);
1875 sub check_checkin_copy_status {
1877 my $copy = $self->copy;
1883 my $status = $U->copy_status($copy->status)->id;
1886 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1887 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1888 $status == OILS_COPY_STATUS_IN_PROCESS ||
1889 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1890 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1891 $status == OILS_COPY_STATUS_CATALOGING ||
1892 $status == OILS_COPY_STATUS_RESHELVING );
1894 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1895 if( $status == OILS_COPY_STATUS_LOST );
1897 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1898 if( $status == OILS_COPY_STATUS_MISSING );
1900 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1905 # --------------------------------------------------------------------------
1906 # On checkin, we need to return as many relevant objects as we can
1907 # --------------------------------------------------------------------------
1908 sub checkin_flesh_events {
1911 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1912 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1913 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1917 for my $evt (@{$self->events}) {
1920 $payload->{copy} = $U->unflesh_copy($self->copy);
1921 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1922 $payload->{circ} = $self->circ;
1923 $payload->{transit} = $self->transit;
1924 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1926 # $self->hold may or may not have been replaced with a
1927 # valid hold after processing a cancelled hold
1928 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1930 $evt->{payload} = $payload;
1935 my( $self, $msg ) = @_;
1936 my $bc = ($self->copy) ? $self->copy->barcode :
1939 my $usr = ($self->patron) ? $self->patron->id : "";
1940 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1941 ", recipient=$usr, copy=$bc");
1947 $self->log_me("do_renew()");
1948 $self->is_renewal(1);
1950 # Make sure there is an open circ to renew that is not
1951 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1952 my $circ = $self->editor->search_action_circulation(
1953 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1956 $circ = $self->editor->search_action_circulation(
1958 target_copy => $self->copy->id,
1959 stop_fines => OILS_STOP_FINES_MAX_FINES,
1960 checkin_time => undef
1965 return $self->bail_on_events($self->editor->event) unless $circ;
1967 # A user is not allowed to renew another user's items without permission
1968 unless( $circ->usr eq $self->editor->requestor->id ) {
1969 return $self->bail_on_events($self->editor->events)
1970 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
1973 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1974 if $circ->renewal_remaining < 1;
1976 # -----------------------------------------------------------------
1978 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1981 $self->run_renew_permit;
1984 $self->do_checkin();
1985 return if $self->bail_out;
1987 unless( $self->permit_override ) {
1989 return if $self->bail_out;
1990 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1991 $self->remove_event('ITEM_NOT_CATALOGED');
1994 $self->override_events;
1995 return if $self->bail_out;
1998 $self->do_checkout();
2003 my( $self, $evt ) = @_;
2004 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2005 $logger->debug("circulator: removing event from list: $evt");
2006 my @events = @{$self->events};
2007 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2012 my( $self, $evt ) = @_;
2013 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2014 return grep { $_->{textcode} eq $evt } @{$self->events};
2019 sub run_renew_permit {
2021 my $runner = $self->script_runner;
2023 $runner->load($self->circ_permit_renew);
2024 my $result = $runner->run or
2025 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2026 my $events = $result->{events};
2028 $logger->activity("ciculator: circ_permit_renew for user ".
2029 $self->patron->id." returned events: @$events") if @$events;
2031 $self->push_events(OpenILS::Event->new($_)) for @$events;
2033 $logger->debug("circulator: re-creating script runner to be safe");
2034 $self->mk_script_runner;