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};
271 # --------------------------------------------------------------------------
272 # This package actually manages all of the circulation logic
273 # --------------------------------------------------------------------------
274 package OpenILS::Application::Circ::Circulator;
275 use strict; use warnings;
276 use vars q/$AUTOLOAD/;
278 use OpenILS::Utils::Fieldmapper;
279 use OpenSRF::Utils::Cache;
280 use Digest::MD5 qw(md5_hex);
281 use DateTime::Format::ISO8601;
282 use OpenILS::Utils::PermitHold;
283 use OpenSRF::Utils qw/:datetime/;
284 use OpenSRF::Utils::SettingsClient;
285 use OpenILS::Application::Circ::Holds;
286 use OpenILS::Application::Circ::Transit;
287 use OpenSRF::Utils::Logger qw(:logger);
288 use OpenILS::Utils::CStoreEditor qw/:funcs/;
289 use OpenILS::Application::Circ::ScriptBuilder;
290 use OpenILS::Const qw/:const/;
292 my $holdcode = "OpenILS::Application::Circ::Holds";
293 my $transcode = "OpenILS::Application::Circ::Transit";
298 # --------------------------------------------------------------------------
299 # Add a pile of automagic getter/setter methods
300 # --------------------------------------------------------------------------
301 my @AUTOLOAD_FIELDS = qw/
316 check_penalty_on_renew
343 recurring_fines_level
356 cancelled_hold_transit
366 my $type = ref($self) or die "$self is not an object";
368 my $name = $AUTOLOAD;
371 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
372 $logger->error("circulator: $type: invalid autoload field: $name");
373 die "$type: invalid autoload field: $name\n"
378 *{"${type}::${name}"} = sub {
381 $s->{$name} = $v if defined $v;
385 return $self->$name($data);
390 my( $class, $auth, %args ) = @_;
391 $class = ref($class) || $class;
392 my $self = bless( {}, $class );
396 new_editor(xact => 1, authtoken => $auth) );
398 unless( $self->editor->checkauth ) {
399 $self->bail_on_events($self->editor->event);
403 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
405 $self->$_($args{$_}) for keys %args;
408 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
410 # if this is a renewal, default to desk_renewal
411 $self->desk_renewal(1) unless
412 $self->opac_renewal or $self->phone_renewal;
418 # --------------------------------------------------------------------------
419 # True if we should discontinue processing
420 # --------------------------------------------------------------------------
422 my( $self, $bool ) = @_;
423 if( defined $bool ) {
424 $logger->info("circulator: BAILING OUT") if $bool;
425 $self->{bail_out} = $bool;
427 return $self->{bail_out};
432 my( $self, @evts ) = @_;
435 $logger->info("circulator: pushing event ".$e->{textcode});
436 push( @{$self->events}, $e ) unless
437 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
443 my $key = md5_hex( time() . rand() . "$$" );
444 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
445 return $self->permit_key($key);
448 sub check_permit_key {
450 my $key = $self->permit_key;
451 return 0 unless $key;
452 my $k = "oils_permit_key_$key";
453 my $one = $self->cache_handle->get_cache($k);
454 $self->cache_handle->delete_cache($k);
455 return ($one) ? 1 : 0;
459 # --------------------------------------------------------------------------
460 # This builds the script runner environment and fetches most of the
462 # --------------------------------------------------------------------------
463 sub mk_script_runner {
469 qw/copy copy_barcode copy_id patron
470 patron_id patron_barcode volume title editor/;
472 # Translate our objects into the ScriptBuilder args hash
473 $$args{$_} = $self->$_() for @fields;
475 $args->{ignore_user_status} = 1 if $self->is_checkin;
476 $$args{fetch_patron_by_circ_copy} = 1;
477 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
479 if( my $pco = $self->pending_checkouts ) {
480 $logger->info("circulator: we were given a pending checkouts number of $pco");
481 $$args{patronItemsOut} = $pco;
484 # This fetches most of the objects we need
485 $self->script_runner(
486 OpenILS::Application::Circ::ScriptBuilder->build($args));
488 # Now we translate the ScriptBuilder objects back into self
489 $self->$_($$args{$_}) for @fields;
491 my @evts = @{$args->{_events}} if $args->{_events};
493 $logger->debug("circulator: script builder returned events: @evts") if @evts;
497 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
498 if(!$self->is_noncat and
500 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
504 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
505 return $self->bail_on_events(@e);
509 $self->is_precat(1) if $self->copy
510 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
512 # We can't renew if there is no copy
513 return $self->bail_on_events(@evts) if
514 $self->is_renewal and !$self->copy;
516 # Set some circ-specific flags in the script environment
517 my $evt = "environment";
518 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
520 if( $self->is_noncat ) {
521 $self->script_runner->insert("$evt.isNonCat", 1);
522 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
525 if( $self->is_precat ) {
526 $self->script_runner->insert("environment.isPrecat", 1, 1);
529 $self->script_runner->add_path( $_ ) for @$script_libs;
537 # --------------------------------------------------------------------------
538 # Does the circ permit work
539 # --------------------------------------------------------------------------
543 $self->log_me("do_permit()");
545 unless( $self->editor->requestor->id == $self->patron->id ) {
546 return $self->bail_on_events($self->editor->event)
547 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
550 $self->check_captured_holds();
551 $self->do_copy_checks();
552 return if $self->bail_out;
553 $self->run_patron_permit_scripts();
554 $self->run_copy_permit_scripts()
555 unless $self->is_precat or $self->is_noncat;
556 $self->override_events() unless
557 $self->is_renewal and not $self->check_penalty_on_renew;
558 return if $self->bail_out;
560 if( $self->is_precat ) {
563 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
564 return $self->bail_out(1) unless $self->is_renewal;
570 payload => $self->mk_permit_key));
574 sub check_captured_holds {
576 my $copy = $self->copy;
577 my $patron = $self->patron;
579 return undef unless $copy;
581 my $s = $U->copy_status($copy->status)->id;
582 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
583 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
585 # Item is on the holds shelf, make sure it's going to the right person
586 my $holds = $self->editor->search_action_hold_request(
589 current_copy => $copy->id ,
590 capture_time => { '!=' => undef },
591 cancel_time => undef,
592 fulfillment_time => undef
598 if( $holds and $$holds[0] ) {
599 return undef if $$holds[0]->usr == $patron->id;
602 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
604 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
610 my $copy = $self->copy;
613 my $stat = $U->copy_status($copy->status)->id;
615 # We cannot check out a copy if it is in-transit
616 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
617 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
620 $self->handle_claims_returned();
621 return if $self->bail_out;
623 # no claims returned circ was found, check if there is any open circ
624 unless( $self->is_renewal ) {
625 my $circs = $self->editor->search_action_circulation(
626 { target_copy => $copy->id, checkin_time => undef }
629 return $self->bail_on_events(
630 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
635 sub send_penalty_request {
637 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
638 $self->penalty_request(
640 'open-ils.penalty.patron_penalty.calculate',
642 authtoken => $self->editor->authtoken,
643 patron => $self->patron } ) );
646 sub gather_penalty_request {
648 return [] unless $self->penalty_request;
649 my $data = $self->penalty_request->recv;
651 throw $data if UNIVERSAL::isa($data,'Error');
652 $data = $data->content;
653 return $data->{fatal_penalties};
655 $logger->error("circulator: penalty request returned no data");
659 # ---------------------------------------------------------------------
660 # This pushes any patron-related events into the list but does not
661 # set bail_out for any events
662 # ---------------------------------------------------------------------
663 sub run_patron_permit_scripts {
665 my $runner = $self->script_runner;
666 my $patronid = $self->patron->id;
668 $self->send_penalty_request() unless
669 $self->is_renewal and not $self->check_penalty_on_renew;
672 # ---------------------------------------------------------------------
673 # Now run the patron permit script
674 # ---------------------------------------------------------------------
675 $runner->load($self->circ_permit_patron);
676 my $result = $runner->run or
677 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
679 my $patron_events = $result->{events};
682 my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ?
683 [] : $self->gather_penalty_request();
685 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
687 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
689 $self->push_events(@allevents);
693 sub run_copy_permit_scripts {
695 my $copy = $self->copy || return;
696 my $runner = $self->script_runner;
698 # ---------------------------------------------------------------------
699 # Capture all of the copy permit events
700 # ---------------------------------------------------------------------
701 $runner->load($self->circ_permit_copy);
702 my $result = $runner->run or
703 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
704 my $copy_events = $result->{events};
706 # ---------------------------------------------------------------------
707 # Now collect all of the events together
708 # ---------------------------------------------------------------------
710 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
712 # See if this copy has an alert message
713 my $ae = $self->check_copy_alert();
714 push( @allevents, $ae ) if $ae;
716 # uniquify the events
717 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
718 @allevents = values %hash;
721 $_->{payload} = $copy if
722 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
725 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
727 $self->push_events(@allevents);
731 sub check_copy_alert {
733 return undef if $self->is_renewal;
734 return OpenILS::Event->new(
735 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
736 if $self->copy and $self->copy->alert_message;
742 # --------------------------------------------------------------------------
743 # If the call is overriding and has permissions to override every collected
744 # event, the are cleared. Any event that the caller does not have
745 # permission to override, will be left in the event list and bail_out will
747 # XXX We need code in here to cancel any holds/transits on copies
748 # that are being force-checked out
749 # --------------------------------------------------------------------------
750 sub override_events {
752 my @events = @{$self->events};
753 return unless @events;
755 if(!$self->override) {
756 return $self->bail_out(1)
757 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
762 for my $e (@events) {
763 my $tc = $e->{textcode};
764 next if $tc eq 'SUCCESS';
765 my $ov = "$tc.override";
766 $logger->info("circulator: attempting to override event: $ov");
768 return $self->bail_on_events($self->editor->event)
769 unless( $self->editor->allowed($ov) );
774 # --------------------------------------------------------------------------
775 # If there is an open claimsreturn circ on the requested copy, close the
776 # circ if overriding, otherwise bail out
777 # --------------------------------------------------------------------------
778 sub handle_claims_returned {
780 my $copy = $self->copy;
782 my $CR = $self->editor->search_action_circulation(
784 target_copy => $copy->id,
785 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
786 checkin_time => undef,
790 return unless ($CR = $CR->[0]);
794 # - If the caller has set the override flag, we will check the item in
795 if($self->override) {
797 $CR->checkin_time('now');
798 $CR->checkin_lib($self->editor->requestor->ws_ou);
799 $CR->checkin_staff($self->editor->requestor->id);
801 $evt = $self->editor->event
802 unless $self->editor->update_action_circulation($CR);
805 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
808 $self->bail_on_events($evt) if $evt;
813 # --------------------------------------------------------------------------
814 # This performs the checkout
815 # --------------------------------------------------------------------------
819 $self->log_me("do_checkout()");
821 # make sure perms are good if this isn't a renewal
822 unless( $self->is_renewal ) {
823 return $self->bail_on_events($self->editor->event)
824 unless( $self->editor->allowed('COPY_CHECKOUT') );
827 # verify the permit key
828 unless( $self->check_permit_key ) {
829 if( $self->permit_override ) {
830 return $self->bail_on_events($self->editor->event)
831 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
833 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
837 # if this is a non-cataloged circ, build the circ and finish
838 if( $self->is_noncat ) {
839 $self->checkout_noncat;
841 OpenILS::Event->new('SUCCESS',
842 payload => { noncat_circ => $self->circ }));
846 if( $self->is_precat ) {
847 $self->script_runner->insert("environment.isPrecat", 1, 1);
848 $self->make_precat_copy;
849 return if $self->bail_out;
851 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
852 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
855 $self->do_copy_checks;
856 return if $self->bail_out;
858 $self->run_checkout_scripts();
859 return if $self->bail_out;
861 $self->build_checkout_circ_object();
862 return if $self->bail_out;
864 $self->apply_modified_due_date();
865 return if $self->bail_out;
867 return $self->bail_on_events($self->editor->event)
868 unless $self->editor->create_action_circulation($self->circ);
870 # refresh the circ to force local time zone for now
871 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
873 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
875 return if $self->bail_out;
877 $self->handle_checkout_holds();
878 return if $self->bail_out;
880 # ------------------------------------------------------------------------------
881 # Update the patron penalty info in the DB. Run it for permit-overrides or
882 # renewals since both of those cases do not require the penalty server to
883 # run during the permit phase of the checkout
884 # ------------------------------------------------------------------------------
885 if( $self->permit_override or $self->is_renewal ) {
886 $U->update_patron_penalties(
887 authtoken => $self->editor->authtoken,
888 patron => $self->patron,
893 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
895 OpenILS::Event->new('SUCCESS',
897 copy => $U->unflesh_copy($self->copy),
900 holds_fulfilled => $self->fulfilled_holds,
908 my $copy = $self->copy;
910 my $stat = $copy->status if ref $copy->status;
911 my $loc = $copy->location if ref $copy->location;
912 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
914 $copy->status($stat->id) if $stat;
915 $copy->location($loc->id) if $loc;
916 $copy->circ_lib($circ_lib->id) if $circ_lib;
917 $copy->editor($self->editor->requestor->id);
918 $copy->edit_date('now');
919 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
921 return $self->bail_on_events($self->editor->event)
922 unless $self->editor->update_asset_copy($self->copy);
924 $copy->status($U->copy_status($copy->status));
925 $copy->location($loc) if $loc;
926 $copy->circ_lib($circ_lib) if $circ_lib;
931 my( $self, @evts ) = @_;
932 $self->push_events(@evts);
936 sub handle_checkout_holds {
939 my $copy = $self->copy;
940 my $patron = $self->patron;
942 my $holds = $self->editor->search_action_hold_request(
944 current_copy => $copy->id ,
945 cancel_time => undef,
946 fulfillment_time => undef
952 # XXX We should only fulfill one hold here...
953 # XXX If a hold was transited to the user who is checking out
954 # the item, we need to make sure that hold is what's grabbed
957 # for now, just sort by id to get what should be the oldest hold
958 $holds = [ sort { $a->id <=> $b->id } @$holds ];
959 my @myholds = grep { $_->usr eq $patron->id } @$holds;
960 my @altholds = grep { $_->usr ne $patron->id } @$holds;
963 my $hold = $myholds[0];
965 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
967 # if the hold was never officially captured, capture it.
968 $hold->capture_time('now') unless $hold->capture_time;
970 # just make sure it's set correctly
971 $hold->current_copy($copy->id);
973 $hold->fulfillment_time('now');
974 $hold->fulfillment_staff($self->editor->requestor->id);
975 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
977 return $self->bail_on_events($self->editor->event)
978 unless $self->editor->update_action_hold_request($hold);
980 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
982 push( @fulfilled, $hold->id );
985 # If there are any holds placed for other users that point to this copy,
986 # then we need to un-target those holds so the targeter can pick a new copy
989 $logger->info("circulator: un-targeting hold ".$_->id.
990 " because copy ".$copy->id." is getting checked out");
992 # - make the targeter process this hold at next run
993 $_->clear_prev_check_time;
995 # - clear out the targetted copy
996 $_->clear_current_copy;
997 $_->clear_capture_time;
999 return $self->bail_on_event($self->editor->event)
1000 unless $self->editor->update_action_hold_request($_);
1004 $self->fulfilled_holds(\@fulfilled);
1009 sub run_checkout_scripts {
1013 my $runner = $self->script_runner;
1014 $runner->load($self->circ_duration);
1016 my $result = $runner->run or
1017 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1019 my $duration = $result->{durationRule};
1020 my $recurring = $result->{recurringFinesRule};
1021 my $max_fine = $result->{maxFine};
1023 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1025 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1026 return $self->bail_on_events($evt) if $evt;
1028 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1029 return $self->bail_on_events($evt) if $evt;
1031 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1032 return $self->bail_on_events($evt) if $evt;
1036 # The item circulates with an unlimited duration
1042 $self->duration_rule($duration);
1043 $self->recurring_fines_rule($recurring);
1044 $self->max_fine_rule($max_fine);
1048 sub build_checkout_circ_object {
1051 my $circ = Fieldmapper::action::circulation->new;
1052 my $duration = $self->duration_rule;
1053 my $max = $self->max_fine_rule;
1054 my $recurring = $self->recurring_fines_rule;
1055 my $copy = $self->copy;
1056 my $patron = $self->patron;
1060 my $dname = $duration->name;
1061 my $mname = $max->name;
1062 my $rname = $recurring->name;
1064 $logger->debug("circulator: building circulation ".
1065 "with duration=$dname, maxfine=$mname, recurring=$rname");
1067 $circ->duration( $duration->shrt )
1068 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1069 $circ->duration( $duration->normal )
1070 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1071 $circ->duration( $duration->extended )
1072 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1074 $circ->recuring_fine( $recurring->low )
1075 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1076 $circ->recuring_fine( $recurring->normal )
1077 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1078 $circ->recuring_fine( $recurring->high )
1079 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1081 $circ->duration_rule( $duration->name );
1082 $circ->recuring_fine_rule( $recurring->name );
1083 $circ->max_fine_rule( $max->name );
1084 $circ->max_fine( $max->amount );
1086 $circ->fine_interval($recurring->recurance_interval);
1087 $circ->renewal_remaining( $duration->max_renewals );
1091 $logger->info("circulator: copy found with an unlimited circ duration");
1092 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1093 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1094 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1095 $circ->renewal_remaining(0);
1098 $circ->target_copy( $copy->id );
1099 $circ->usr( $patron->id );
1100 $circ->circ_lib( $self->circ_lib );
1102 if( $self->is_renewal ) {
1103 $circ->opac_renewal('t') if $self->opac_renewal;
1104 $circ->phone_renewal('t') if $self->phone_renewal;
1105 $circ->desk_renewal('t') if $self->desk_renewal;
1106 $circ->renewal_remaining($self->renewal_remaining);
1107 $circ->circ_staff($self->editor->requestor->id);
1111 # if the user provided an overiding checkout time,
1112 # (e.g. the checkout really happened several hours ago), then
1113 # we apply that here. Does this need a perm??
1114 $circ->xact_start(clense_ISO8601($self->checkout_time))
1115 if $self->checkout_time;
1117 # if a patron is renewing, 'requestor' will be the patron
1118 $circ->circ_staff($self->editor->requestor->id);
1119 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1125 sub apply_modified_due_date {
1127 my $circ = $self->circ;
1128 my $copy = $self->copy;
1130 if( $self->due_date ) {
1132 return $self->bail_on_events($self->editor->event)
1133 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1135 $circ->due_date(clense_ISO8601($self->due_date));
1139 # if the due_date lands on a day when the location is closed
1140 return unless $copy and $circ->due_date;
1142 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1144 # due-date overlap should be determined by the location the item
1145 # is checked out from, not the owning or circ lib of the item
1146 my $org = $self->editor->requestor->ws_ou;
1148 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1149 " with an item due date of ".$circ->due_date );
1151 my $dateinfo = $U->storagereq(
1152 'open-ils.storage.actor.org_unit.closed_date.overlap',
1153 $org, $circ->due_date );
1156 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1157 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1159 # XXX make the behavior more dynamic
1160 # for now, we just push the due date to after the close date
1161 $circ->due_date($dateinfo->{end});
1168 sub create_due_date {
1169 my( $self, $duration ) = @_;
1170 my ($sec,$min,$hour,$mday,$mon,$year) =
1171 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1172 $year += 1900; $mon += 1;
1173 my $due_date = sprintf(
1174 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1175 $year, $mon, $mday, $hour, $min, $sec);
1181 sub make_precat_copy {
1183 my $copy = $self->copy;
1186 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1188 $copy->editor($self->editor->requestor->id);
1189 $copy->edit_date('now');
1190 $copy->dummy_title($self->dummy_title);
1191 $copy->dummy_author($self->dummy_author);
1193 $self->update_copy();
1197 $logger->info("circulator: Creating a new precataloged ".
1198 "copy in checkout with barcode " . $self->copy_barcode);
1200 $copy = Fieldmapper::asset::copy->new;
1201 $copy->circ_lib($self->circ_lib);
1202 $copy->creator($self->editor->requestor->id);
1203 $copy->editor($self->editor->requestor->id);
1204 $copy->barcode($self->copy_barcode);
1205 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1206 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1207 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1209 $copy->dummy_title($self->dummy_title || "");
1210 $copy->dummy_author($self->dummy_author || "");
1212 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1214 $self->push_events($self->editor->event);
1218 # this is a little bit of a hack, but we need to
1219 # get the copy into the script runner
1220 $self->script_runner->insert("environment.copy", $copy, 1);
1224 sub checkout_noncat {
1230 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1231 my $count = $self->noncat_count || 1;
1232 my $cotime = clense_ISO8601($self->checkout_time) || "";
1234 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1238 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1239 $self->editor->requestor->id,
1247 $self->push_events($evt);
1258 $self->log_me("do_checkin()");
1261 return $self->bail_on_events(
1262 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1265 if( $self->checkin_check_holds_shelf() ) {
1266 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1267 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1268 $self->checkin_flesh_events;
1272 unless( $self->is_renewal ) {
1273 return $self->bail_on_events($self->editor->event)
1274 unless $self->editor->allowed('COPY_CHECKIN');
1277 $self->push_events($self->check_copy_alert());
1278 $self->push_events($self->check_checkin_copy_status());
1280 # the renew code will have already found our circulation object
1281 unless( $self->is_renewal and $self->circ ) {
1282 my $circs = $self->editor->search_action_circulation(
1283 { target_copy => $self->copy->id, checkin_time => undef });
1284 $self->circ($$circs[0]);
1286 # for now, just warn if there are multiple open circs on a copy
1287 $logger->warn("circulator: we have ".scalar(@$circs).
1288 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1291 # if the circ is marked as 'claims returned', add the event to the list
1292 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1293 if ($self->circ and $self->circ->stop_fines
1294 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1296 # handle the overridable events
1297 $self->override_events unless $self->is_renewal;
1298 return if $self->bail_out;
1302 $self->editor->search_action_transit_copy(
1303 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1307 $self->checkin_handle_circ;
1308 return if $self->bail_out;
1309 $self->checkin_changed(1);
1311 } elsif( $self->transit ) {
1312 my $hold_transit = $self->process_received_transit;
1313 $self->checkin_changed(1);
1315 if( $self->bail_out ) {
1316 $self->checkin_flesh_events;
1320 if( my $e = $self->check_checkin_copy_status() ) {
1321 # If the original copy status is special, alert the caller
1322 my $ev = $self->events;
1323 $self->events([$e]);
1324 $self->override_events;
1325 return if $self->bail_out;
1329 if( $hold_transit or
1330 $U->copy_status($self->copy->status)->id
1331 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1334 if( $hold_transit ) {
1335 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1337 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1342 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1344 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1345 $self->reshelve_copy(1);
1346 $self->cancelled_hold_transit(1);
1347 $self->notify_hold(0); # don't notify for cancelled holds
1348 return if $self->bail_out;
1352 # hold transited to correct location
1353 $self->checkin_flesh_events;
1358 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1360 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1361 " that is in-transit, but there is no transit.. repairing");
1362 $self->reshelve_copy(1);
1363 return if $self->bail_out;
1366 if( $self->is_renewal ) {
1367 $self->push_events(OpenILS::Event->new('SUCCESS'));
1371 # ------------------------------------------------------------------------------
1372 # Circulations and transits are now closed where necessary. Now go on to see if
1373 # this copy can fulfill a hold or needs to be routed to a different location
1374 # ------------------------------------------------------------------------------
1376 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1377 return if $self->bail_out;
1379 } else { # not needed for a hold
1381 my $circ_lib = (ref $self->copy->circ_lib) ?
1382 $self->copy->circ_lib->id : $self->copy->circ_lib;
1384 if( $self->remote_hold ) {
1385 $circ_lib = $self->remote_hold->pickup_lib;
1386 $logger->warn("circulator: Copy ".$self->copy->barcode.
1387 " is on a remote hold's shelf, sending to $circ_lib");
1390 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1392 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1394 $self->checkin_handle_precat();
1395 return if $self->bail_out;
1399 my $bc = $self->copy->barcode;
1400 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1401 $self->checkin_build_copy_transit($circ_lib);
1402 return if $self->bail_out;
1403 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1407 $self->reshelve_copy;
1408 return if $self->bail_out;
1410 unless($self->checkin_changed) {
1412 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1413 my $stat = $U->copy_status($self->copy->status)->id;
1415 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1416 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1417 $self->bail_out(1); # no need to commit anything
1421 $self->push_events(OpenILS::Event->new('SUCCESS'))
1422 unless @{$self->events};
1426 # ------------------------------------------------------------------------------
1427 # Update the patron penalty info in the DB
1428 # ------------------------------------------------------------------------------
1429 $U->update_patron_penalties(
1430 authtoken => $self->editor->authtoken,
1431 patron => $self->patron,
1432 background => 1 ) if $self->is_checkin;
1434 $self->checkin_flesh_events;
1440 my $force = $self->force || shift;
1441 my $copy = $self->copy;
1443 my $stat = $U->copy_status($copy->status)->id;
1446 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1447 $stat != OILS_COPY_STATUS_CATALOGING and
1448 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1449 $stat != OILS_COPY_STATUS_RESHELVING )) {
1451 $copy->status( OILS_COPY_STATUS_RESHELVING );
1453 $self->checkin_changed(1);
1458 # Returns true if the item is at the current location
1459 # because it was transited there for a hold and the
1460 # hold has not been fulfilled
1461 sub checkin_check_holds_shelf {
1463 return 0 unless $self->copy;
1466 $U->copy_status($self->copy->status)->id ==
1467 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1469 # find the hold that put us on the holds shelf
1470 my $holds = $self->editor->search_action_hold_request(
1472 current_copy => $self->copy->id,
1473 capture_time => { '!=' => undef },
1474 fulfillment_time => undef,
1475 cancel_time => undef,
1480 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1481 $self->reshelve_copy(1);
1485 my $hold = $$holds[0];
1487 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1488 $hold->id. "] for copy ".$self->copy->barcode);
1490 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1491 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1495 $logger->info("circulator: hold is not for here..");
1496 $self->remote_hold($hold);
1501 sub checkin_handle_precat {
1503 my $copy = $self->copy;
1505 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1506 $copy->status(OILS_COPY_STATUS_CATALOGING);
1507 $self->update_copy();
1508 $self->checkin_changed(1);
1509 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1514 sub checkin_build_copy_transit {
1517 my $copy = $self->copy;
1518 my $transit = Fieldmapper::action::transit_copy->new;
1520 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1521 $logger->info("circulator: transiting copy to $dest");
1523 $transit->source($self->editor->requestor->ws_ou);
1524 $transit->dest($dest);
1525 $transit->target_copy($copy->id);
1526 $transit->source_send_time('now');
1527 $transit->copy_status( $U->copy_status($copy->status)->id );
1529 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1531 return $self->bail_on_events($self->editor->event)
1532 unless $self->editor->create_action_transit_copy($transit);
1534 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1536 $self->checkin_changed(1);
1540 sub attempt_checkin_hold_capture {
1542 my $copy = $self->copy;
1544 # See if this copy can fulfill any holds
1545 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1546 $self->editor, $copy, $self->editor->requestor );
1549 $logger->debug("circulator: no potential permitted".
1550 "holds found for copy ".$copy->barcode);
1554 $self->retarget($retarget);
1556 $logger->info("circulator: found permitted hold ".
1557 $hold->id . " for copy, capturing...");
1559 $hold->current_copy($copy->id);
1560 $hold->capture_time('now');
1562 # prevent DB errors caused by fetching
1563 # holds from storage, and updating through cstore
1564 $hold->clear_fulfillment_time;
1565 $hold->clear_fulfillment_staff;
1566 $hold->clear_fulfillment_lib;
1567 $hold->clear_expire_time;
1568 $hold->clear_cancel_time;
1569 $hold->clear_prev_check_time unless $hold->prev_check_time;
1571 $self->bail_on_events($self->editor->event)
1572 unless $self->editor->update_action_hold_request($hold);
1574 $self->checkin_changed(1);
1576 return 1 if $self->bail_out;
1578 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1580 # This hold was captured in the correct location
1581 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1582 $self->push_events(OpenILS::Event->new('SUCCESS'));
1584 #$self->do_hold_notify($hold->id);
1585 $self->notify_hold($hold->id);
1589 # Hold needs to be picked up elsewhere. Build a hold
1590 # transit and route the item.
1591 $self->checkin_build_hold_transit();
1592 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1593 return 1 if $self->bail_out;
1595 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1598 # make sure we save the copy status
1603 sub do_hold_notify {
1604 my( $self, $holdid ) = @_;
1606 $logger->info("circulator: running delayed hold notify process");
1608 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1609 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1611 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1612 hold_id => $holdid, requestor => $self->editor->requestor);
1614 $logger->debug("circulator: built hold notifier");
1616 if(!$notifier->event) {
1618 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1620 my $stat = $notifier->send_email_notify;
1621 if( $stat == '1' ) {
1622 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1626 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1629 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1633 sub retarget_holds {
1634 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1635 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1636 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1637 # no reason to wait for the return value
1641 sub checkin_build_hold_transit {
1644 my $copy = $self->copy;
1645 my $hold = $self->hold;
1646 my $trans = Fieldmapper::action::hold_transit_copy->new;
1648 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1650 $trans->hold($hold->id);
1651 $trans->source($self->editor->requestor->ws_ou);
1652 $trans->dest($hold->pickup_lib);
1653 $trans->source_send_time("now");
1654 $trans->target_copy($copy->id);
1656 # when the copy gets to its destination, it will recover
1657 # this status - put it onto the holds shelf
1658 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1660 return $self->bail_on_events($self->editor->event)
1661 unless $self->editor->create_action_hold_transit_copy($trans);
1666 sub process_received_transit {
1668 my $copy = $self->copy;
1669 my $copyid = $self->copy->id;
1671 my $status_name = $U->copy_status($copy->status)->name;
1672 $logger->debug("circulator: attempting transit receive on ".
1673 "copy $copyid. Copy status is $status_name");
1675 my $transit = $self->transit;
1677 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1678 # - this item is in-transit to a different location
1680 my $tid = $transit->id;
1681 my $loc = $self->editor->requestor->ws_ou;
1682 my $dest = $transit->dest;
1684 $logger->info("circulator: Fowarding transit on copy which is destined ".
1685 "for a different location. transit=$tid, copy=$copyid, current ".
1686 "location=$loc, destination location=$dest");
1688 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1690 # grab the associated hold object if available
1691 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1692 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1694 return $self->bail_on_events($evt);
1697 # The transit is received, set the receive time
1698 $transit->dest_recv_time('now');
1699 $self->bail_on_events($self->editor->event)
1700 unless $self->editor->update_action_transit_copy($transit);
1702 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1704 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1705 $copy->status( $transit->copy_status );
1706 $self->update_copy();
1707 return if $self->bail_out;
1711 #$self->do_hold_notify($hold_transit->hold);
1712 $self->notify_hold($hold_transit->hold);
1717 OpenILS::Event->new(
1720 payload => { transit => $transit, holdtransit => $hold_transit } ));
1722 return $hold_transit;
1726 sub checkin_handle_circ {
1730 my $circ = $self->circ;
1731 my $copy = $self->copy;
1735 # backdate the circ if necessary
1736 if($self->backdate) {
1737 $self->checkin_handle_backdate;
1738 return if $self->bail_out;
1741 if(!$circ->stop_fines) {
1742 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1743 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1744 $circ->stop_fines_time('now') unless $self->backdate;
1745 $circ->stop_fines_time($self->backdate) if $self->backdate;
1748 # see if there are any fines owed on this circ. if not, close it
1749 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1750 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1752 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1754 # Set the checkin vars since we have the item
1755 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1757 $circ->checkin_staff($self->editor->requestor->id);
1758 $circ->checkin_lib($self->editor->requestor->ws_ou);
1760 my $circ_lib = (ref $self->copy->circ_lib) ?
1761 $self->copy->circ_lib->id : $self->copy->circ_lib;
1762 my $stat = $U->copy_status($self->copy->status)->id;
1764 # If the item is lost/missing and it needs to be sent home, don't
1765 # reshelve the copy, leave it lost/missing so the recipient will know
1766 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1767 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1768 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1771 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1775 return $self->bail_on_events($self->editor->event)
1776 unless $self->editor->update_action_circulation($circ);
1780 sub checkin_handle_backdate {
1783 my $bd = $self->backdate;
1785 # ------------------------------------------------------------------
1786 # clean up the backdate for date comparison
1787 # we want any bills created on or after the backdate
1788 # ------------------------------------------------------------------
1789 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1790 #$bd = "${bd}T23:59:59";
1792 my $bills = $self->editor->search_money_billing(
1794 billing_ts => { '>=' => $bd },
1795 xact => $self->circ->id,
1796 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1800 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1802 for my $bill (@$bills) {
1803 unless( $U->is_true($bill->voided) ) {
1804 $logger->info("backdate voiding bill ".$bill->id);
1806 $bill->void_time('now');
1807 $bill->voider($self->editor->requestor->id);
1808 my $n = $bill->note || "";
1809 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1811 $self->bail_on_events($self->editor->event)
1812 unless $self->editor->update_money_billing($bill);
1820 # XXX Legacy version for Circ.pm support
1821 sub _checkin_handle_backdate {
1822 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1825 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1826 $bd = "${bd}T23:59:59";
1828 my $bills = $session->request(
1829 "open-ils.storage.direct.money.billing.search_where.atomic",
1830 billing_ts => { '>=' => $bd },
1832 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1835 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1838 for my $bill (@$bills) {
1839 unless( $U->is_true($bill->voided) ) {
1840 $logger->debug("voiding bill ".$bill->id);
1842 $bill->void_time('now');
1843 $bill->voider($requestor->id);
1844 my $n = $bill->note || "";
1845 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1846 my $s = $session->request(
1847 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1848 return $U->DB_UPDATE_FAILED($bill) unless $s;
1862 sub find_patron_from_copy {
1864 my $circs = $self->editor->search_action_circulation(
1865 { target_copy => $self->copy->id, checkin_time => undef });
1866 my $circ = $circs->[0];
1867 return unless $circ;
1868 my $u = $self->editor->retrieve_actor_user($circ->usr)
1869 or return $self->bail_on_events($self->editor->event);
1873 sub check_checkin_copy_status {
1875 my $copy = $self->copy;
1881 my $status = $U->copy_status($copy->status)->id;
1884 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1885 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1886 $status == OILS_COPY_STATUS_IN_PROCESS ||
1887 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1888 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1889 $status == OILS_COPY_STATUS_CATALOGING ||
1890 $status == OILS_COPY_STATUS_RESHELVING );
1892 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1893 if( $status == OILS_COPY_STATUS_LOST );
1895 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1896 if( $status == OILS_COPY_STATUS_MISSING );
1898 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1903 # --------------------------------------------------------------------------
1904 # On checkin, we need to return as many relevant objects as we can
1905 # --------------------------------------------------------------------------
1906 sub checkin_flesh_events {
1909 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1910 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1911 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1915 for my $evt (@{$self->events}) {
1918 $payload->{copy} = $U->unflesh_copy($self->copy);
1919 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1920 $payload->{circ} = $self->circ;
1921 $payload->{transit} = $self->transit;
1922 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1924 # $self->hold may or may not have been replaced with a
1925 # valid hold after processing a cancelled hold
1926 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1928 $evt->{payload} = $payload;
1933 my( $self, $msg ) = @_;
1934 my $bc = ($self->copy) ? $self->copy->barcode :
1937 my $usr = ($self->patron) ? $self->patron->id : "";
1938 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1939 ", recipient=$usr, copy=$bc");
1945 $self->log_me("do_renew()");
1946 $self->is_renewal(1);
1948 # Make sure there is an open circ to renew that is not
1949 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1950 my $circ = $self->editor->search_action_circulation(
1951 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1954 $circ = $self->editor->search_action_circulation(
1956 target_copy => $self->copy->id,
1957 stop_fines => OILS_STOP_FINES_MAX_FINES,
1958 checkin_time => undef
1963 return $self->bail_on_events($self->editor->event) unless $circ;
1965 # A user is not allowed to renew another user's items without permission
1966 unless( $circ->usr eq $self->editor->requestor->id ) {
1967 return $self->bail_on_events($self->editor->events)
1968 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
1971 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1972 if $circ->renewal_remaining < 1;
1974 # -----------------------------------------------------------------
1976 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1979 $self->run_renew_permit;
1982 $self->do_checkin();
1983 return if $self->bail_out;
1985 unless( $self->permit_override ) {
1987 return if $self->bail_out;
1988 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1989 $self->remove_event('ITEM_NOT_CATALOGED');
1992 $self->override_events;
1993 return if $self->bail_out;
1996 $self->do_checkout();
2001 my( $self, $evt ) = @_;
2002 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2003 $logger->debug("circulator: removing event from list: $evt");
2004 my @events = @{$self->events};
2005 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2010 my( $self, $evt ) = @_;
2011 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2012 return grep { $_->{textcode} eq $evt } @{$self->events};
2017 sub run_renew_permit {
2019 my $runner = $self->script_runner;
2021 $runner->load($self->circ_permit_renew);
2022 my $result = $runner->run or
2023 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2024 my $events = $result->{events};
2026 $logger->activity("ciculator: circ_permit_renew for user ".
2027 $self->patron->id." returned events: @$events") if @$events;
2029 $self->push_events(OpenILS::Event->new($_)) for @$events;
2031 $logger->debug("circulator: re-creating script runner to be safe");
2032 $self->mk_script_runner;