1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenSRF::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/;
15 my $conf = OpenSRF::Utils::SettingsClient->new;
16 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
17 my @pfx = ( @pfx2, "scripts" );
19 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
20 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
21 my $d = $conf->config_value( @pfx, 'circ_duration' );
22 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
23 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
24 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
25 my $lb = $conf->config_value( @pfx2, 'script_path' );
27 $logger->error( "Missing circ script(s)" )
28 unless( $p and $c and $d and $f and $m and $pr );
30 $scripts{circ_permit_patron} = $p;
31 $scripts{circ_permit_copy} = $c;
32 $scripts{circ_duration} = $d;
33 $scripts{circ_recurring_fines}= $f;
34 $scripts{circ_max_fines} = $m;
35 $scripts{circ_permit_renew} = $pr;
37 $lb = [ $lb ] unless ref($lb);
41 "circulator: Loaded rules scripts for circ: " .
42 "circ permit patron = $p, ".
43 "circ permit copy = $c, ".
44 "circ duration = $d, ".
45 "circ recurring fines = $f, " .
46 "circ max fines = $m, ".
47 "circ renew permit = $pr. ".
52 __PACKAGE__->register_method(
53 method => "run_method",
54 api_name => "open-ils.circ.checkout.permit",
56 Determines if the given checkout can occur
57 @param authtoken The login session key
58 @param params A trailing hash of named params including
59 barcode : The copy barcode,
60 patron : The patron the checkout is occurring for,
61 renew : true or false - whether or not this is a renewal
62 @return The event that occurred during the permit check.
66 __PACKAGE__->register_method (
67 method => 'run_method',
68 api_name => 'open-ils.circ.checkout.permit.override',
69 signature => q/@see open-ils.circ.checkout.permit/,
73 __PACKAGE__->register_method(
74 method => "run_method",
75 api_name => "open-ils.circ.checkout",
78 @param authtoken The login session key
79 @param params A named hash of params including:
81 barcode If no copy is provided, the copy is retrieved via barcode
82 copyid If no copy or barcode is provide, the copy id will be use
83 patron The patron's id
84 noncat True if this is a circulation for a non-cataloted item
85 noncat_type The non-cataloged type id
86 noncat_circ_lib The location for the noncat circ.
87 precat The item has yet to be cataloged
88 dummy_title The temporary title of the pre-cataloded item
89 dummy_author The temporary authr of the pre-cataloded item
90 Default is the home org of the staff member
91 @return The SUCCESS event on success, any other event depending on the error
94 __PACKAGE__->register_method(
95 method => "run_method",
96 api_name => "open-ils.circ.checkin",
99 Generic super-method for handling all copies
100 @param authtoken The login session key
101 @param params Hash of named parameters including:
102 barcode - The copy barcode
103 force - If true, copies in bad statuses will be checked in and give good statuses
108 __PACKAGE__->register_method(
109 method => "run_method",
110 api_name => "open-ils.circ.checkin.override",
111 signature => q/@see open-ils.circ.checkin/
114 __PACKAGE__->register_method(
115 method => "run_method",
116 api_name => "open-ils.circ.renew.override",
117 signature => q/@see open-ils.circ.renew/,
121 __PACKAGE__->register_method(
122 method => "run_method",
123 api_name => "open-ils.circ.renew",
124 notes => <<" NOTES");
125 PARAMS( authtoken, circ => circ_id );
126 open-ils.circ.renew(login_session, circ_object);
127 Renews the provided circulation. login_session is the requestor of the
128 renewal and if the logged in user is not the same as circ->usr, then
129 the logged in user must have RENEW_CIRC permissions.
132 __PACKAGE__->register_method(
133 method => "run_method",
134 api_name => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136 method => "run_method",
137 api_name => "open-ils.circ.checkout.full.override");
142 my( $self, $conn, $auth, $args ) = @_;
143 translate_legacy_args($args);
144 my $api = $self->api_name;
147 OpenILS::Application::Circ::Circulator->new($auth, %$args);
149 return circ_events($circulator) if $circulator->bail_out;
151 # --------------------------------------------------------------------------
152 # Go ahead and load the script runner to make sure we have all
153 # of the objects we need
154 # --------------------------------------------------------------------------
155 $circulator->is_renewal(1) if $api =~ /renew/;
156 $circulator->is_checkin(1) if $api =~ /checkin/;
157 $circulator->mk_script_runner;
158 return circ_events($circulator) if $circulator->bail_out;
160 $circulator->circ_permit_patron($scripts{circ_permit_patron});
161 $circulator->circ_permit_copy($scripts{circ_permit_copy});
162 $circulator->circ_duration($scripts{circ_duration});
163 $circulator->circ_permit_renew($scripts{circ_permit_renew});
165 $circulator->override(1) if $api =~ /override/o;
167 if( $api =~ /checkout\.permit/ ) {
168 $circulator->do_permit();
170 } elsif( $api =~ /checkout.full/ ) {
172 $circulator->do_permit();
173 unless( $circulator->bail_out ) {
174 $circulator->events([]);
175 $circulator->do_checkout();
178 } elsif( $api =~ /checkout/ ) {
179 $circulator->do_checkout();
181 } elsif( $api =~ /checkin/ ) {
182 $circulator->do_checkin();
184 } elsif( $api =~ /renew/ ) {
185 $circulator->is_renewal(1);
186 $circulator->do_renew();
189 if( $circulator->bail_out ) {
192 # make sure no success event accidentally slip in
194 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
195 my @e = @{$circulator->events};
196 push( @ee, $_->{textcode} ) for @e;
197 $logger->info("circulator: bailing out with events: @ee");
198 $circulator->editor->rollback;
201 $circulator->editor->commit;
204 $circulator->script_runner->cleanup;
206 return circ_events($circulator);
211 my @e = @{$circ->events};
212 return (@e == 1) ? $e[0] : \@e;
217 sub translate_legacy_args {
220 if( $$args{barcode} ) {
221 $$args{copy_barcode} = $$args{barcode};
222 delete $$args{barcode};
225 if( $$args{copyid} ) {
226 $$args{copy_id} = $$args{copyid};
227 delete $$args{copyid};
230 if( $$args{patronid} ) {
231 $$args{patron_id} = $$args{patronid};
232 delete $$args{patronid};
235 if( $$args{patron} and !ref($$args{patron}) ) {
236 $$args{patron_id} = $$args{patron};
237 delete $$args{patron};
241 if( $$args{noncat} ) {
242 $$args{is_noncat} = $$args{noncat};
243 delete $$args{noncat};
246 if( $$args{precat} ) {
247 $$args{is_precat} = $$args{precat};
248 delete $$args{precat};
254 # --------------------------------------------------------------------------
255 # This package actually manages all of the circulation logic
256 # --------------------------------------------------------------------------
257 package OpenILS::Application::Circ::Circulator;
258 use strict; use warnings;
259 use vars q/$AUTOLOAD/;
261 use OpenILS::Utils::Fieldmapper;
262 use OpenSRF::Utils::Cache;
263 use Digest::MD5 qw(md5_hex);
264 use DateTime::Format::ISO8601;
265 use OpenILS::Utils::PermitHold;
266 use OpenSRF::Utils qw/:datetime/;
267 use OpenSRF::Utils::SettingsClient;
268 use OpenILS::Application::Circ::Holds;
269 use OpenILS::Application::Circ::Transit;
270 use OpenSRF::Utils::Logger qw(:logger);
271 use OpenILS::Utils::CStoreEditor qw/:funcs/;
272 use OpenILS::Application::Circ::ScriptBuilder;
273 use OpenILS::Const qw/:const/;
275 my $U = "OpenILS::Application::AppUtils";
276 my $holdcode = "OpenILS::Application::Circ::Holds";
277 my $transcode = "OpenILS::Application::Circ::Transit";
282 # --------------------------------------------------------------------------
283 # Add a pile of automagic getter/setter methods
284 # --------------------------------------------------------------------------
285 my @AUTOLOAD_FIELDS = qw/
325 recurring_fines_level
342 my $type = ref($self) or die "$self is not an object";
344 my $name = $AUTOLOAD;
347 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
348 $logger->error("circulator: $type: invalid autoload field: $name");
349 die "$type: invalid autoload field: $name\n"
354 *{"${type}::${name}"} = sub {
357 $s->{$name} = $v if defined $v;
361 return $self->$name($data);
366 my( $class, $auth, %args ) = @_;
367 $class = ref($class) || $class;
368 my $self = bless( {}, $class );
372 new_editor(xact => 1, authtoken => $auth) );
374 unless( $self->editor->checkauth ) {
375 $self->bail_on_events($self->editor->event);
379 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
381 $self->$_($args{$_}) for keys %args;
384 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
390 # --------------------------------------------------------------------------
391 # True if we should discontinue processing
392 # --------------------------------------------------------------------------
394 my( $self, $bool ) = @_;
395 if( defined $bool ) {
396 $logger->info("circulator: BAILING OUT") if $bool;
397 $self->{bail_out} = $bool;
399 return $self->{bail_out};
404 my( $self, @evts ) = @_;
407 $logger->info("circulator: pushing event ".$e->{textcode});
408 push( @{$self->events}, $e ) unless
409 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
415 my $key = md5_hex( time() . rand() . "$$" );
416 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
417 return $self->permit_key($key);
420 sub check_permit_key {
422 my $key = $self->permit_key;
423 return 0 unless $key;
424 my $k = "oils_permit_key_$key";
425 my $one = $self->cache_handle->get_cache($k);
426 $self->cache_handle->delete_cache($k);
427 return ($one) ? 1 : 0;
431 # --------------------------------------------------------------------------
432 # This builds the script runner environment and fetches most of the
434 # --------------------------------------------------------------------------
435 sub mk_script_runner {
441 qw/copy copy_barcode copy_id patron
442 patron_id patron_barcode volume title editor/;
444 # Translate our objects into the ScriptBuilder args hash
445 $$args{$_} = $self->$_() for @fields;
447 $args->{ignore_user_status} = 1 if $self->is_checkin;
448 $$args{fetch_patron_by_circ_copy} = 1;
449 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
451 # This fetches most of the objects we need
452 $self->script_runner(
453 OpenILS::Application::Circ::ScriptBuilder->build($args));
455 # Now we translate the ScriptBuilder objects back into self
456 $self->$_($$args{$_}) for @fields;
458 my @evts = @{$args->{_events}} if $args->{_events};
460 $logger->debug("circulator: script builder returned events: @evts") if @evts;
464 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
465 if(!$self->is_noncat and
467 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
471 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
472 return $self->bail_on_events(@e);
476 $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
478 # We can't renew if there is no copy
479 return $self->bail_on_events(@evts) if
480 $self->is_renewal and !$self->copy;
482 # Set some circ-specific flags in the script environment
483 my $evt = "environment";
484 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
486 if( $self->is_noncat ) {
487 $self->script_runner->insert("$evt.isNonCat", 1);
488 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
491 if( $self->is_precat ) {
492 $self->script_runner->insert("environment.isPrecat", 1, 1);
495 $self->script_runner->add_path( $_ ) for @$script_libs;
503 # --------------------------------------------------------------------------
504 # Does the circ permit work
505 # --------------------------------------------------------------------------
509 $self->log_me("do_permit()");
511 unless( $self->editor->requestor->id == $self->patron->id ) {
512 return $self->bail_on_events($self->editor->event)
513 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
517 $self->check_captured_holds();
518 $self->do_copy_checks();
519 return if $self->bail_out;
520 $self->run_patron_permit_scripts();
521 $self->run_copy_permit_scripts()
522 unless $self->is_precat or $self->is_noncat;
523 $self->override_events() unless $self->is_renewal;
524 return if $self->bail_out;
526 if( $self->is_precat ) {
529 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
530 return $self->bail_out(1) unless $self->is_renewal;
536 payload => $self->mk_permit_key));
540 sub check_captured_holds {
542 my $copy = $self->copy;
543 my $patron = $self->patron;
545 return undef unless $copy;
547 my $s = $U->copy_status($copy->status)->id;
548 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
549 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
551 # Item is on the holds shelf, make sure it's going to the right person
552 my $holds = $self->editor->search_action_hold_request(
555 current_copy => $copy->id ,
556 capture_time => { '!=' => undef },
557 cancel_time => undef,
558 fulfillment_time => undef
564 if( $holds and $$holds[0] ) {
565 return undef if $$holds[0]->usr == $patron->id;
568 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
570 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
576 my $copy = $self->copy;
579 my $stat = $U->copy_status($copy->status)->id;
581 # We cannot check out a copy if it is in-transit
582 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
583 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
586 $self->handle_claims_returned();
587 return if $self->bail_out;
589 # no claims returned circ was found, check if there is any open circ
590 unless( $self->is_renewal ) {
591 my $circs = $self->editor->search_action_circulation(
592 { target_copy => $copy->id, checkin_time => undef }
595 return $self->bail_on_events(
596 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
601 sub send_penalty_request {
603 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
604 $self->penalty_request(
606 'open-ils.penalty.patron_penalty.calculate',
608 authtoken => $self->editor->authtoken,
609 patron => $self->patron } ) );
612 sub gather_penalty_request {
614 return [] unless $self->penalty_request;
615 my $data = $self->penalty_request->recv;
617 $data = $data->content;
618 return $data->{fatal_penalties};
620 $logger->error("circulator: penalty request returned no data");
624 # ---------------------------------------------------------------------
625 # This pushes any patron-related events into the list but does not
626 # set bail_out for any events
627 # ---------------------------------------------------------------------
628 sub run_patron_permit_scripts {
630 my $runner = $self->script_runner;
631 my $patronid = $self->patron->id;
633 $self->send_penalty_request();
635 # ---------------------------------------------------------------------
636 # Now run the patron permit script
637 # ---------------------------------------------------------------------
638 $runner->load($self->circ_permit_patron);
639 my $result = $runner->run or
640 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
642 my $patron_events = $result->{events};
645 my $penalties = $self->gather_penalty_request();
647 for my $p (@$penalties, @$patron_events) {
649 # this is policy directly in the code, not a good idea in general, but
650 # the penalty server doesn't know anything about renewals, so we
651 # have to strip the event out here
652 next if $self->is_renewal and $p eq 'PATRON_EXCEEDS_OVERDUE_COUNT';
655 push( @allevents, OpenILS::Event->new($p))
658 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
660 $self->push_events(@allevents);
664 sub run_copy_permit_scripts {
666 my $copy = $self->copy || return;
667 my $runner = $self->script_runner;
669 # ---------------------------------------------------------------------
670 # Capture all of the copy permit events
671 # ---------------------------------------------------------------------
672 $runner->load($self->circ_permit_copy);
673 my $result = $runner->run or
674 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
675 my $copy_events = $result->{events};
677 # ---------------------------------------------------------------------
678 # Now collect all of the events together
679 # ---------------------------------------------------------------------
681 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
683 # See if this copy has an alert message
684 my $ae = $self->check_copy_alert();
685 push( @allevents, $ae ) if $ae;
687 # uniquify the events
688 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
689 @allevents = values %hash;
692 $_->{payload} = $copy if
693 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
696 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
698 $self->push_events(@allevents);
702 sub check_copy_alert {
704 return undef if $self->is_renewal;
705 return OpenILS::Event->new(
706 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
707 if $self->copy and $self->copy->alert_message;
713 # --------------------------------------------------------------------------
714 # If the call is overriding and has permissions to override every collected
715 # event, the are cleared. Any event that the caller does not have
716 # permission to override, will be left in the event list and bail_out will
718 # XXX We need code in here to cancel any holds/transits on copies
719 # that are being force-checked out
720 # --------------------------------------------------------------------------
721 sub override_events {
723 my @events = @{$self->events};
724 return unless @events;
726 if(!$self->override) {
727 return $self->bail_out(1)
728 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
733 for my $e (@events) {
734 my $tc = $e->{textcode};
735 next if $tc eq 'SUCCESS';
736 my $ov = "$tc.override";
737 $logger->info("circulator: attempting to override event: $ov");
739 return $self->bail_on_events($self->editor->event)
740 unless( $self->editor->allowed($ov) );
745 # --------------------------------------------------------------------------
746 # If there is an open claimsreturn circ on the requested copy, close the
747 # circ if overriding, otherwise bail out
748 # --------------------------------------------------------------------------
749 sub handle_claims_returned {
751 my $copy = $self->copy;
753 my $CR = $self->editor->search_action_circulation(
755 target_copy => $copy->id,
756 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
757 checkin_time => undef,
761 return unless ($CR = $CR->[0]);
765 # - If the caller has set the override flag, we will check the item in
766 if($self->override) {
768 $CR->checkin_time('now');
769 $CR->checkin_lib($self->editor->requestor->ws_ou);
770 $CR->checkin_staff($self->editor->requestor->id);
772 $evt = $self->editor->event
773 unless $self->editor->update_action_circulation($CR);
776 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
779 $self->bail_on_events($evt) if $evt;
784 # --------------------------------------------------------------------------
785 # This performs the checkout
786 # --------------------------------------------------------------------------
790 $self->log_me("do_checkout()");
792 # make sure perms are good if this isn't a renewal
793 unless( $self->is_renewal ) {
794 return $self->bail_on_events($self->editor->event)
795 unless( $self->editor->allowed('COPY_CHECKOUT') );
798 # verify the permit key
799 unless( $self->check_permit_key ) {
800 if( $self->permit_override ) {
801 return $self->bail_on_events($self->editor->event)
802 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
804 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
808 # if this is a non-cataloged circ, build the circ and finish
809 if( $self->is_noncat ) {
810 $self->checkout_noncat;
812 OpenILS::Event->new('SUCCESS',
813 payload => { noncat_circ => $self->circ }));
817 if( $self->is_precat ) {
818 $self->script_runner->insert("environment.isPrecat", 1, 1);
819 $self->make_precat_copy;
820 return if $self->bail_out;
822 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
823 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
826 $self->do_copy_checks;
827 return if $self->bail_out;
829 $self->run_checkout_scripts();
830 return if $self->bail_out;
832 $self->build_checkout_circ_object();
833 return if $self->bail_out;
835 $self->apply_modified_due_date();
836 return if $self->bail_out;
838 return $self->bail_on_events($self->editor->event)
839 unless $self->editor->create_action_circulation($self->circ);
841 # refresh the circ to force local time zone for now
842 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
844 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
846 return if $self->bail_out;
848 $self->handle_checkout_holds();
849 return if $self->bail_out;
851 # ------------------------------------------------------------------------------
852 # Update the patron penalty info in the DB
853 # ------------------------------------------------------------------------------
854 $U->update_patron_penalties(
855 authtoken => $self->editor->authtoken,
856 patron => $self->patron,
860 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
862 OpenILS::Event->new('SUCCESS',
864 copy => $U->unflesh_copy($self->copy),
867 holds_fulfilled => $self->fulfilled_holds,
875 my $copy = $self->copy;
877 my $stat = $copy->status if ref $copy->status;
878 my $loc = $copy->location if ref $copy->location;
879 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
881 $copy->status($stat->id) if $stat;
882 $copy->location($loc->id) if $loc;
883 $copy->circ_lib($circ_lib->id) if $circ_lib;
884 $copy->editor($self->editor->requestor->id);
885 $copy->edit_date('now');
886 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
888 return $self->bail_on_events($self->editor->event)
889 unless $self->editor->update_asset_copy($self->copy);
891 $copy->status($U->copy_status($copy->status));
892 $copy->location($loc) if $loc;
893 $copy->circ_lib($circ_lib) if $circ_lib;
898 my( $self, @evts ) = @_;
899 $self->push_events(@evts);
903 sub handle_checkout_holds {
906 my $copy = $self->copy;
907 my $patron = $self->patron;
909 my $holds = $self->editor->search_action_hold_request(
911 current_copy => $copy->id ,
912 cancel_time => undef,
913 fulfillment_time => undef
919 # XXX We should only fulfill one hold here...
920 # XXX If a hold was transited to the user who is checking out
921 # the item, we need to make sure that hold is what's grabbed
924 # for now, just sort by id to get what should be the oldest hold
925 $holds = [ sort { $a->id <=> $b->id } @$holds ];
926 my @myholds = grep { $_->usr eq $patron->id } @$holds;
927 my @altholds = grep { $_->usr ne $patron->id } @$holds;
930 my $hold = $myholds[0];
932 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
934 # if the hold was never officially captured, capture it.
935 $hold->capture_time('now') unless $hold->capture_time;
937 # just make sure it's set correctly
938 $hold->current_copy($copy->id);
940 $hold->fulfillment_time('now');
941 $hold->fulfillment_staff($self->editor->requestor->id);
942 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
944 return $self->bail_on_events($self->editor->event)
945 unless $self->editor->update_action_hold_request($hold);
947 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
949 push( @fulfilled, $hold->id );
952 # If there are any holds placed for other users that point to this copy,
953 # then we need to un-target those holds so the targeter can pick a new copy
956 $logger->info("circulator: un-targeting hold ".$_->id.
957 " because copy ".$copy->id." is getting checked out");
959 # - make the targeter process this hold at next run
960 $_->clear_prev_check_time;
962 # - clear out the targetted copy
963 $_->clear_current_copy;
964 $_->clear_capture_time;
966 return $self->bail_on_event($self->editor->event)
967 unless $self->editor->update_action_hold_request($_);
971 $self->fulfilled_holds(\@fulfilled);
976 sub run_checkout_scripts {
980 my $runner = $self->script_runner;
981 $runner->load($self->circ_duration);
983 my $result = $runner->run or
984 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
986 my $duration = $result->{durationRule};
987 my $recurring = $result->{recurringFinesRule};
988 my $max_fine = $result->{maxFine};
990 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
992 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
993 return $self->bail_on_events($evt) if $evt;
995 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
996 return $self->bail_on_events($evt) if $evt;
998 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
999 return $self->bail_on_events($evt) if $evt;
1003 # The item circulates with an unlimited duration
1009 $self->duration_rule($duration);
1010 $self->recurring_fines_rule($recurring);
1011 $self->max_fine_rule($max_fine);
1015 sub build_checkout_circ_object {
1018 my $circ = Fieldmapper::action::circulation->new;
1019 my $duration = $self->duration_rule;
1020 my $max = $self->max_fine_rule;
1021 my $recurring = $self->recurring_fines_rule;
1022 my $copy = $self->copy;
1023 my $patron = $self->patron;
1027 my $dname = $duration->name;
1028 my $mname = $max->name;
1029 my $rname = $recurring->name;
1031 $logger->debug("circulator: building circulation ".
1032 "with duration=$dname, maxfine=$mname, recurring=$rname");
1034 $circ->duration( $duration->shrt )
1035 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1036 $circ->duration( $duration->normal )
1037 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1038 $circ->duration( $duration->extended )
1039 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1041 $circ->recuring_fine( $recurring->low )
1042 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1043 $circ->recuring_fine( $recurring->normal )
1044 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1045 $circ->recuring_fine( $recurring->high )
1046 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1048 $circ->duration_rule( $duration->name );
1049 $circ->recuring_fine_rule( $recurring->name );
1050 $circ->max_fine_rule( $max->name );
1051 $circ->max_fine( $max->amount );
1053 $circ->fine_interval($recurring->recurance_interval);
1054 $circ->renewal_remaining( $duration->max_renewals );
1058 $logger->info("circulator: copy found with an unlimited circ duration");
1059 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1060 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1061 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1062 $circ->renewal_remaining(0);
1065 $circ->target_copy( $copy->id );
1066 $circ->usr( $patron->id );
1067 $circ->circ_lib( $self->circ_lib );
1069 if( $self->is_renewal ) {
1070 $circ->opac_renewal(1);
1071 $circ->renewal_remaining($self->renewal_remaining);
1072 $circ->circ_staff($self->editor->requestor->id);
1075 # if the user provided an overiding checkout time,
1076 # (e.g. the checkout really happened several hours ago), then
1077 # we apply that here. Does this need a perm??
1078 $circ->xact_start(clense_ISO8601($self->checkout_time))
1079 if $self->checkout_time;
1081 # if a patron is renewing, 'requestor' will be the patron
1082 $circ->circ_staff($self->editor->requestor->id);
1083 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1089 sub apply_modified_due_date {
1091 my $circ = $self->circ;
1092 my $copy = $self->copy;
1094 if( $self->due_date ) {
1096 return $self->bail_on_events($self->editor->event)
1097 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1099 $circ->due_date(clense_ISO8601($self->due_date));
1103 # if the due_date lands on a day when the location is closed
1104 return unless $copy and $circ->due_date;
1106 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1108 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1109 " with an item due date of ".$circ->due_date );
1111 my $dateinfo = $U->storagereq(
1112 'open-ils.storage.actor.org_unit.closed_date.overlap',
1113 $org, $circ->due_date );
1116 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1117 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1119 # XXX make the behavior more dynamic
1120 # for now, we just push the due date to after the close date
1121 $circ->due_date($dateinfo->{end});
1128 sub create_due_date {
1129 my( $self, $duration ) = @_;
1130 my ($sec,$min,$hour,$mday,$mon,$year) =
1131 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1132 $year += 1900; $mon += 1;
1133 my $due_date = sprintf(
1134 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1135 $year, $mon, $mday, $hour, $min, $sec);
1141 sub make_precat_copy {
1143 my $copy = $self->copy;
1146 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1148 $copy->editor($self->editor->requestor->id);
1149 $copy->edit_date('now');
1150 $copy->dummy_title($self->dummy_title);
1151 $copy->dummy_author($self->dummy_author);
1153 $self->update_copy();
1157 $logger->info("circulator: Creating a new precataloged ".
1158 "copy in checkout with barcode " . $self->copy_barcode);
1160 $copy = Fieldmapper::asset::copy->new;
1161 $copy->circ_lib($self->circ_lib);
1162 $copy->creator($self->editor->requestor->id);
1163 $copy->editor($self->editor->requestor->id);
1164 $copy->barcode($self->copy_barcode);
1165 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1166 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1167 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1169 $copy->dummy_title($self->dummy_title || "");
1170 $copy->dummy_author($self->dummy_author || "");
1172 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1174 $self->push_events($self->editor->event);
1178 # this is a little bit of a hack, but we need to
1179 # get the copy into the script runner
1180 $self->script_runner->insert("environment.copy", $copy, 1);
1184 sub checkout_noncat {
1190 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1191 my $count = $self->noncat_count || 1;
1192 my $cotime = clense_ISO8601($self->checkout_time) || "";
1194 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1198 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1199 $self->editor->requestor->id,
1207 $self->push_events($evt);
1218 $self->log_me("do_checkin()");
1221 return $self->bail_on_events(
1222 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1225 if( $self->checkin_check_holds_shelf() ) {
1226 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1227 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1228 $self->checkin_flesh_events;
1232 unless( $self->is_renewal ) {
1233 return $self->bail_on_events($self->editor->event)
1234 unless $self->editor->allowed('COPY_CHECKIN');
1237 $self->push_events($self->check_copy_alert());
1238 $self->push_events($self->check_checkin_copy_status());
1240 # the renew code will have already found our circulation object
1241 unless( $self->is_renewal and $self->circ ) {
1243 $self->editor->search_action_circulation(
1244 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1247 # if the circ is marked as 'claims returned', add the event to the list
1248 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1249 if ($self->circ and $self->circ->stop_fines
1250 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1252 # handle the overridable events
1253 $self->override_events unless $self->is_renewal;
1254 return if $self->bail_out;
1258 $self->editor->search_action_transit_copy(
1259 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1263 $self->checkin_handle_circ;
1264 return if $self->bail_out;
1265 $self->checkin_changed(1);
1267 } elsif( $self->transit ) {
1268 my $hold_transit = $self->process_received_transit;
1269 $self->checkin_changed(1);
1271 if( $self->bail_out ) {
1272 $self->checkin_flesh_events;
1276 if( my $e = $self->check_checkin_copy_status() ) {
1277 # If the original copy status is special, alert the caller
1278 my $ev = $self->events;
1279 $self->events([$e]);
1280 $self->override_events;
1281 return if $self->bail_out;
1285 if( $hold_transit or
1286 $U->copy_status($self->copy->status)->id
1287 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1290 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1291 $U->fetch_open_hold_by_copy($self->copy->id)
1294 $self->checkin_flesh_events;
1298 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1299 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1300 " that is in-transit, but there is no transit.. repairing");
1301 $self->reshelve_copy(1);
1302 return if $self->bail_out;
1305 if( $self->is_renewal ) {
1306 $self->push_events(OpenILS::Event->new('SUCCESS'));
1310 # ------------------------------------------------------------------------------
1311 # Circulations and transits are now closed where necessary. Now go on to see if
1312 # this copy can fulfill a hold or needs to be routed to a different location
1313 # ------------------------------------------------------------------------------
1315 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1316 return if $self->bail_out;
1318 } else { # not needed for a hold
1321 my $circ_lib = (ref $self->copy->circ_lib) ?
1322 $self->copy->circ_lib->id : $self->copy->circ_lib;
1324 if( $self->remote_hold ) {
1325 $circ_lib = $self->remote_hold->pickup_lib;
1326 $logger->warn("circulator: Copy ".$self->copy->barcode.
1327 " is on a remote hold's shelf, sending to $circ_lib");
1330 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1332 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1334 $self->checkin_handle_precat();
1335 return if $self->bail_out;
1339 my $bc = $self->copy->barcode;
1340 $logger->info("circulator: copy $bc at a remote lib - sending home");
1341 $self->checkin_build_copy_transit();
1342 return if $self->bail_out;
1343 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1347 $self->reshelve_copy;
1348 return if $self->bail_out;
1350 unless($self->checkin_changed) {
1352 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1353 my $stat = $U->copy_status($self->copy->status)->id;
1355 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1356 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1357 $self->bail_out(1); # no need to commit anything
1360 $self->push_events(OpenILS::Event->new('SUCCESS'))
1361 unless @{$self->events};
1365 # ------------------------------------------------------------------------------
1366 # Update the patron penalty info in the DB
1367 # ------------------------------------------------------------------------------
1368 $U->update_patron_penalties(
1369 authtoken => $self->editor->authtoken,
1370 patron => $self->patron,
1371 background => 1 ) if $self->is_checkin;
1373 $self->checkin_flesh_events;
1379 my $force = $self->force || shift;
1380 my $copy = $self->copy;
1382 my $stat = $U->copy_status($copy->status)->id;
1385 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1386 $stat != OILS_COPY_STATUS_CATALOGING and
1387 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1388 $stat != OILS_COPY_STATUS_RESHELVING )) {
1390 $copy->status( OILS_COPY_STATUS_RESHELVING );
1392 $self->checkin_changed(1);
1397 # Returns true if the item is at the current location
1398 # because it was transited there for a hold and the
1399 # hold has not been fulfilled
1400 sub checkin_check_holds_shelf {
1402 return 0 unless $self->copy;
1405 $U->copy_status($self->copy->status)->id ==
1406 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1408 # find the hold that put us on the holds shelf
1409 my $holds = $self->editor->search_action_hold_request(
1411 current_copy => $self->copy->id,
1412 capture_time => { '!=' => undef },
1413 fulfillment_time => undef,
1414 cancel_time => undef,
1419 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1420 $self->reshelve_copy(1);
1424 my $hold = $$holds[0];
1426 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1427 $hold->id. "] for copy ".$self->copy->barcode);
1429 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1430 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1434 $logger->info("circulator: hold is not for here..");
1435 $self->remote_hold($hold);
1440 sub checkin_handle_precat {
1442 my $copy = $self->copy;
1444 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1445 $copy->status(OILS_COPY_STATUS_CATALOGING);
1446 $self->update_copy();
1447 $self->checkin_changed(1);
1448 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1453 sub checkin_build_copy_transit {
1455 my $copy = $self->copy;
1456 my $transit = Fieldmapper::action::transit_copy->new;
1458 $transit->source($self->editor->requestor->ws_ou);
1459 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1460 $transit->target_copy($copy->id);
1461 $transit->source_send_time('now');
1462 $transit->copy_status( $U->copy_status($copy->status)->id );
1464 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1466 return $self->bail_on_events($self->editor->event)
1467 unless $self->editor->create_action_transit_copy($transit);
1469 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1471 $self->checkin_changed(1);
1475 sub attempt_checkin_hold_capture {
1477 my $copy = $self->copy;
1479 # See if this copy can fulfill any holds
1480 my ($hold) = $holdcode->find_nearest_permitted_hold(
1481 OpenSRF::AppSession->create('open-ils.storage'),
1482 $copy, $self->editor->requestor );
1485 $logger->debug("circulator: no potential permitted".
1486 "holds found for copy ".$copy->barcode);
1491 $logger->info("circulator: found permitted hold ".
1492 $hold->id . " for copy, capturing...");
1494 $hold->current_copy($copy->id);
1495 $hold->capture_time('now');
1497 # prevent DB errors caused by fetching
1498 # holds from storage, and updating through cstore
1499 $hold->clear_fulfillment_time;
1500 $hold->clear_fulfillment_staff;
1501 $hold->clear_fulfillment_lib;
1502 $hold->clear_expire_time;
1503 $hold->clear_cancel_time;
1504 $hold->clear_prev_check_time unless $hold->prev_check_time;
1506 $self->bail_on_events($self->editor->event)
1507 unless $self->editor->update_action_hold_request($hold);
1509 $self->checkin_changed(1);
1511 return 1 if $self->bail_out;
1513 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1515 # This hold was captured in the correct location
1516 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1517 $self->push_events(OpenILS::Event->new('SUCCESS'));
1519 $self->do_hold_notify($hold->id);
1523 # Hold needs to be picked up elsewhere. Build a hold
1524 # transit and route the item.
1525 $self->checkin_build_hold_transit();
1526 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1527 return 1 if $self->bail_out;
1529 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1532 # make sure we save the copy status
1537 sub do_hold_notify {
1538 my( $self, $holdid ) = @_;
1539 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1540 editor => $self->editor, hold_id => $holdid );
1542 if(!$notifier->event) {
1544 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1546 my $stat = $notifier->send_email_notify;
1547 $logger->info("ciculator: hold notify succeeded for hold $holdid") if $stat eq '1';
1548 $logger->warn("ciculator: * hold notify failed for hold $holdid") if $stat ne '1';
1551 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1556 sub checkin_build_hold_transit {
1560 my $copy = $self->copy;
1561 my $hold = $self->hold;
1562 my $trans = Fieldmapper::action::hold_transit_copy->new;
1564 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1566 $trans->hold($hold->id);
1567 $trans->source($self->editor->requestor->ws_ou);
1568 $trans->dest($hold->pickup_lib);
1569 $trans->source_send_time("now");
1570 $trans->target_copy($copy->id);
1572 # when the copy gets to its destination, it will recover
1573 # this status - put it onto the holds shelf
1574 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1576 return $self->bail_on_events($self->editor->event)
1577 unless $self->editor->create_action_hold_transit_copy($trans);
1582 sub process_received_transit {
1584 my $copy = $self->copy;
1585 my $copyid = $self->copy->id;
1587 my $status_name = $U->copy_status($copy->status)->name;
1588 $logger->debug("circulator: attempting transit receive on ".
1589 "copy $copyid. Copy status is $status_name");
1591 my $transit = $self->transit;
1593 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1594 $logger->info("circulator: Fowarding transit on copy which is destined ".
1595 "for a different location. copy=$copyid,current ".
1596 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1598 return $self->bail_on_events(
1599 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1602 # The transit is received, set the receive time
1603 $transit->dest_recv_time('now');
1604 $self->bail_on_events($self->editor->event)
1605 unless $self->editor->update_action_transit_copy($transit);
1607 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1609 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1610 $copy->status( $transit->copy_status );
1611 $self->update_copy();
1612 return if $self->bail_out;
1616 $self->do_hold_notify($hold_transit->hold);
1621 OpenILS::Event->new(
1624 payload => { transit => $transit, holdtransit => $hold_transit } ));
1626 return $hold_transit;
1630 sub checkin_handle_circ {
1634 my $circ = $self->circ;
1635 my $copy = $self->copy;
1639 # backdate the circ if necessary
1640 if($self->backdate) {
1641 $self->checkin_handle_backdate;
1642 return if $self->bail_out;
1645 if(!$circ->stop_fines) {
1646 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1647 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1648 $circ->stop_fines_time('now') unless $self->backdate;
1649 $circ->stop_fines_time($self->backdate) if $self->backdate;
1652 # see if there are any fines owed on this circ. if not, close it
1653 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1654 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1656 # Set the checkin vars since we have the item
1657 $circ->checkin_time('now');
1658 $circ->checkin_staff($self->editor->requestor->id);
1659 $circ->checkin_lib($self->editor->requestor->ws_ou);
1661 my $circ_lib = (ref $self->copy->circ_lib) ?
1662 $self->copy->circ_lib->id : $self->copy->circ_lib;
1663 my $stat = $U->copy_status($self->copy->status)->id;
1665 # If the item is lost/missing and it needs to be sent home, don't
1666 # reshelve the copy, leave it lost/missing so the recipient will know
1667 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1668 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1669 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1672 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1677 return $self->bail_on_events($self->editor->event)
1678 unless $self->editor->update_action_circulation($circ);
1682 sub checkin_handle_backdate {
1685 my $bd = $self->backdate;
1686 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1687 $bd = "${bd}T23:59:59";
1689 my $bills = $self->editor->search_money_billing(
1691 billing_ts => { '>=' => $bd },
1692 xact => $self->circ->id,
1693 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1697 for my $bill (@$bills) {
1698 if( !$bill->voided or $bill->voided =~ /f/i ) {
1700 my $n = $bill->note || "";
1701 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1703 $self->bail_on_events($self->editor->event)
1704 unless $self->editor->update_money_billing($bill);
1711 # XXX Legacy version for Circ.pm support
1712 sub _checkin_handle_backdate {
1713 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1716 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1717 $bd = "${bd}T23:59:59";
1720 my $bills = $session->request(
1721 "open-ils.storage.direct.money.billing.search_where.atomic",
1722 billing_ts => { '>=' => $bd },
1724 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1728 for my $bill (@$bills) {
1730 my $n = $bill->note || "";
1731 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1732 my $s = $session->request(
1733 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1734 return $U->DB_UPDATE_FAILED($bill) unless $s;
1744 sub find_patron_from_copy {
1746 my $circs = $self->editor->search_action_circulation(
1747 { target_copy => $self->copy->id, checkin_time => undef });
1748 my $circ = $circs->[0];
1749 return unless $circ;
1750 my $u = $self->editor->retrieve_actor_user($circ->usr)
1751 or return $self->bail_on_events($self->editor->event);
1755 sub check_checkin_copy_status {
1757 my $copy = $self->copy;
1763 my $status = $U->copy_status($copy->status)->id;
1766 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1767 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1768 $status == OILS_COPY_STATUS_IN_PROCESS ||
1769 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1770 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1771 $status == OILS_COPY_STATUS_CATALOGING ||
1772 $status == OILS_COPY_STATUS_RESHELVING );
1774 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1775 if( $status == OILS_COPY_STATUS_LOST );
1777 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1778 if( $status == OILS_COPY_STATUS_MISSING );
1780 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1785 # --------------------------------------------------------------------------
1786 # On checkin, we need to return as many relevant objects as we can
1787 # --------------------------------------------------------------------------
1788 sub checkin_flesh_events {
1791 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1792 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1793 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1797 for my $evt (@{$self->events}) {
1800 $payload->{copy} = $U->unflesh_copy($self->copy);
1801 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1802 $payload->{circ} = $self->circ;
1803 $payload->{transit} = $self->transit;
1804 $payload->{hold} = $self->hold;
1806 $evt->{payload} = $payload;
1811 my( $self, $msg ) = @_;
1812 my $bc = ($self->copy) ? $self->copy->barcode :
1815 my $usr = ($self->patron) ? $self->patron->id : "";
1816 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1817 ", recipient=$usr, copy=$bc");
1823 $self->log_me("do_renew()");
1824 $self->is_renewal(1);
1826 unless( $self->is_renewal ) {
1827 return $self->bail_on_events($self->editor->events)
1828 unless $self->editor->allowed('RENEW_CIRC');
1831 # Make sure there is an open circ to renew that is not
1832 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1833 my $circ = $self->editor->search_action_circulation(
1834 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1837 $circ = $self->editor->search_action_circulation(
1839 target_copy => $self->copy->id,
1840 stop_fines => OILS_STOP_FINES_MAX_FINES,
1841 checkin_time => undef
1846 return $self->bail_on_events($self->editor->event) unless $circ;
1848 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1849 if $circ->renewal_remaining < 1;
1851 # -----------------------------------------------------------------
1853 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1856 $self->run_renew_permit;
1859 $self->do_checkin();
1860 return if $self->bail_out;
1862 unless( $self->permit_override ) {
1864 return if $self->bail_out;
1865 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1866 $self->remove_event('ITEM_NOT_CATALOGED');
1869 $self->override_events;
1870 return if $self->bail_out;
1873 $self->do_checkout();
1878 my( $self, $evt ) = @_;
1879 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1880 $logger->debug("circulator: removing event from list: $evt");
1881 my @events = @{$self->events};
1882 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1887 my( $self, $evt ) = @_;
1888 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1889 return grep { $_->{textcode} eq $evt } @{$self->events};
1894 sub run_renew_permit {
1896 my $runner = $self->script_runner;
1898 $runner->load($self->circ_permit_renew);
1899 my $result = $runner->run or
1900 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1901 my $events = $result->{events};
1903 $logger->activity("ciculator: circ_permit_renew for user ".
1904 $self->patron->id." returned events: @$events") if @$events;
1906 $self->push_events(OpenILS::Event->new($_)) for @$events;
1908 $logger->debug("circulator: re-creating script runner to be safe");
1909 $self->mk_script_runner;