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} ]);
197 my @e = @{$circulator->events};
198 push( @ee, $_->{textcode} ) for @e;
199 $logger->info("circulator: bailing out with events: @ee");
201 $circulator->editor->rollback;
204 $circulator->editor->commit;
207 $circulator->script_runner->cleanup;
209 $conn->respond_complete(circ_events($circulator));
211 unless($circulator->bail_out) {
212 $circulator->do_hold_notify($circulator->notify_hold)
213 if $circulator->notify_hold;
219 my @e = @{$circ->events};
220 # if we have multiple events, SUCCESS should not be one of them;
221 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
222 return (@e == 1) ? $e[0] : \@e;
227 sub translate_legacy_args {
230 if( $$args{barcode} ) {
231 $$args{copy_barcode} = $$args{barcode};
232 delete $$args{barcode};
235 if( $$args{copyid} ) {
236 $$args{copy_id} = $$args{copyid};
237 delete $$args{copyid};
240 if( $$args{patronid} ) {
241 $$args{patron_id} = $$args{patronid};
242 delete $$args{patronid};
245 if( $$args{patron} and !ref($$args{patron}) ) {
246 $$args{patron_id} = $$args{patron};
247 delete $$args{patron};
251 if( $$args{noncat} ) {
252 $$args{is_noncat} = $$args{noncat};
253 delete $$args{noncat};
256 if( $$args{precat} ) {
257 $$args{is_precat} = $$args{precat};
258 delete $$args{precat};
265 # --------------------------------------------------------------------------
266 # This package actually manages all of the circulation logic
267 # --------------------------------------------------------------------------
268 package OpenILS::Application::Circ::Circulator;
269 use strict; use warnings;
270 use vars q/$AUTOLOAD/;
272 use OpenILS::Utils::Fieldmapper;
273 use OpenSRF::Utils::Cache;
274 use Digest::MD5 qw(md5_hex);
275 use DateTime::Format::ISO8601;
276 use OpenILS::Utils::PermitHold;
277 use OpenSRF::Utils qw/:datetime/;
278 use OpenSRF::Utils::SettingsClient;
279 use OpenILS::Application::Circ::Holds;
280 use OpenILS::Application::Circ::Transit;
281 use OpenSRF::Utils::Logger qw(:logger);
282 use OpenILS::Utils::CStoreEditor qw/:funcs/;
283 use OpenILS::Application::Circ::ScriptBuilder;
284 use OpenILS::Const qw/:const/;
286 my $U = "OpenILS::Application::AppUtils";
287 my $holdcode = "OpenILS::Application::Circ::Holds";
288 my $transcode = "OpenILS::Application::Circ::Transit";
293 # --------------------------------------------------------------------------
294 # Add a pile of automagic getter/setter methods
295 # --------------------------------------------------------------------------
296 my @AUTOLOAD_FIELDS = qw/
337 recurring_fines_level
350 cancelled_hold_transit
359 my $type = ref($self) or die "$self is not an object";
361 my $name = $AUTOLOAD;
364 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
365 $logger->error("circulator: $type: invalid autoload field: $name");
366 die "$type: invalid autoload field: $name\n"
371 *{"${type}::${name}"} = sub {
374 $s->{$name} = $v if defined $v;
378 return $self->$name($data);
383 my( $class, $auth, %args ) = @_;
384 $class = ref($class) || $class;
385 my $self = bless( {}, $class );
389 new_editor(xact => 1, authtoken => $auth) );
391 unless( $self->editor->checkauth ) {
392 $self->bail_on_events($self->editor->event);
396 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
398 $self->$_($args{$_}) for keys %args;
401 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
403 # if this is a renewal, default to desk_renewal
404 $self->desk_renewal(1) unless
405 $self->opac_renewal or $self->phone_renewal;
411 # --------------------------------------------------------------------------
412 # True if we should discontinue processing
413 # --------------------------------------------------------------------------
415 my( $self, $bool ) = @_;
416 if( defined $bool ) {
417 $logger->info("circulator: BAILING OUT") if $bool;
418 $self->{bail_out} = $bool;
420 return $self->{bail_out};
425 my( $self, @evts ) = @_;
428 $logger->info("circulator: pushing event ".$e->{textcode});
429 push( @{$self->events}, $e ) unless
430 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
436 my $key = md5_hex( time() . rand() . "$$" );
437 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
438 return $self->permit_key($key);
441 sub check_permit_key {
443 my $key = $self->permit_key;
444 return 0 unless $key;
445 my $k = "oils_permit_key_$key";
446 my $one = $self->cache_handle->get_cache($k);
447 $self->cache_handle->delete_cache($k);
448 return ($one) ? 1 : 0;
452 # --------------------------------------------------------------------------
453 # This builds the script runner environment and fetches most of the
455 # --------------------------------------------------------------------------
456 sub mk_script_runner {
462 qw/copy copy_barcode copy_id patron
463 patron_id patron_barcode volume title editor/;
465 # Translate our objects into the ScriptBuilder args hash
466 $$args{$_} = $self->$_() for @fields;
468 $args->{ignore_user_status} = 1 if $self->is_checkin;
469 $$args{fetch_patron_by_circ_copy} = 1;
470 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
472 if( my $pco = $self->pending_checkouts ) {
473 $logger->info("circulator: we were given a pending checkouts number of $pco");
474 $$args{patronItemsOut} = $pco;
477 # This fetches most of the objects we need
478 $self->script_runner(
479 OpenILS::Application::Circ::ScriptBuilder->build($args));
481 # Now we translate the ScriptBuilder objects back into self
482 $self->$_($$args{$_}) for @fields;
484 my @evts = @{$args->{_events}} if $args->{_events};
486 $logger->debug("circulator: script builder returned events: @evts") if @evts;
490 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
491 if(!$self->is_noncat and
493 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
497 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
498 return $self->bail_on_events(@e);
502 $self->is_precat(1) if $self->copy
503 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
505 # We can't renew if there is no copy
506 return $self->bail_on_events(@evts) if
507 $self->is_renewal and !$self->copy;
509 # Set some circ-specific flags in the script environment
510 my $evt = "environment";
511 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
513 if( $self->is_noncat ) {
514 $self->script_runner->insert("$evt.isNonCat", 1);
515 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
518 if( $self->is_precat ) {
519 $self->script_runner->insert("environment.isPrecat", 1, 1);
522 $self->script_runner->add_path( $_ ) for @$script_libs;
530 # --------------------------------------------------------------------------
531 # Does the circ permit work
532 # --------------------------------------------------------------------------
536 $self->log_me("do_permit()");
538 unless( $self->editor->requestor->id == $self->patron->id ) {
539 return $self->bail_on_events($self->editor->event)
540 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
543 $self->check_captured_holds();
544 $self->do_copy_checks();
545 return if $self->bail_out;
546 $self->run_patron_permit_scripts();
547 $self->run_copy_permit_scripts()
548 unless $self->is_precat or $self->is_noncat;
549 $self->override_events() unless $self->is_renewal;
550 return if $self->bail_out;
552 if( $self->is_precat ) {
555 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
556 return $self->bail_out(1) unless $self->is_renewal;
562 payload => $self->mk_permit_key));
566 sub check_captured_holds {
568 my $copy = $self->copy;
569 my $patron = $self->patron;
571 return undef unless $copy;
573 my $s = $U->copy_status($copy->status)->id;
574 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
575 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
577 # Item is on the holds shelf, make sure it's going to the right person
578 my $holds = $self->editor->search_action_hold_request(
581 current_copy => $copy->id ,
582 capture_time => { '!=' => undef },
583 cancel_time => undef,
584 fulfillment_time => undef
590 if( $holds and $$holds[0] ) {
591 return undef if $$holds[0]->usr == $patron->id;
594 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
596 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
602 my $copy = $self->copy;
605 my $stat = $U->copy_status($copy->status)->id;
607 # We cannot check out a copy if it is in-transit
608 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
609 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
612 $self->handle_claims_returned();
613 return if $self->bail_out;
615 # no claims returned circ was found, check if there is any open circ
616 unless( $self->is_renewal ) {
617 my $circs = $self->editor->search_action_circulation(
618 { target_copy => $copy->id, checkin_time => undef }
621 return $self->bail_on_events(
622 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
627 sub send_penalty_request {
629 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
630 $self->penalty_request(
632 'open-ils.penalty.patron_penalty.calculate',
634 authtoken => $self->editor->authtoken,
635 patron => $self->patron } ) );
638 sub gather_penalty_request {
640 return [] unless $self->penalty_request;
641 my $data = $self->penalty_request->recv;
643 throw $data if UNIVERSAL::isa($data,'Error');
644 $data = $data->content;
645 return $data->{fatal_penalties};
647 $logger->error("circulator: penalty request returned no data");
651 # ---------------------------------------------------------------------
652 # This pushes any patron-related events into the list but does not
653 # set bail_out for any events
654 # ---------------------------------------------------------------------
655 sub run_patron_permit_scripts {
657 my $runner = $self->script_runner;
658 my $patronid = $self->patron->id;
660 $self->send_penalty_request() unless $self->is_renewal;
662 # ---------------------------------------------------------------------
663 # Now run the patron permit script
664 # ---------------------------------------------------------------------
665 $runner->load($self->circ_permit_patron);
666 my $result = $runner->run or
667 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
669 my $patron_events = $result->{events};
673 # ---------------------------------------------------------------------
674 # this is policy directly in the code, not a good idea in general, but
675 # the penalty server doesn't know anything about renewals, so we
676 # have to strip the event out here
677 my $penalties = ($self->is_renewal) ? [] : $self->gather_penalty_request();
678 # ---------------------------------------------------------------------
680 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
682 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
684 $self->push_events(@allevents);
688 sub run_copy_permit_scripts {
690 my $copy = $self->copy || return;
691 my $runner = $self->script_runner;
693 # ---------------------------------------------------------------------
694 # Capture all of the copy permit events
695 # ---------------------------------------------------------------------
696 $runner->load($self->circ_permit_copy);
697 my $result = $runner->run or
698 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
699 my $copy_events = $result->{events};
701 # ---------------------------------------------------------------------
702 # Now collect all of the events together
703 # ---------------------------------------------------------------------
705 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
707 # See if this copy has an alert message
708 my $ae = $self->check_copy_alert();
709 push( @allevents, $ae ) if $ae;
711 # uniquify the events
712 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
713 @allevents = values %hash;
716 $_->{payload} = $copy if
717 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
720 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
722 $self->push_events(@allevents);
726 sub check_copy_alert {
728 return undef if $self->is_renewal;
729 return OpenILS::Event->new(
730 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
731 if $self->copy and $self->copy->alert_message;
737 # --------------------------------------------------------------------------
738 # If the call is overriding and has permissions to override every collected
739 # event, the are cleared. Any event that the caller does not have
740 # permission to override, will be left in the event list and bail_out will
742 # XXX We need code in here to cancel any holds/transits on copies
743 # that are being force-checked out
744 # --------------------------------------------------------------------------
745 sub override_events {
747 my @events = @{$self->events};
748 return unless @events;
750 if(!$self->override) {
751 return $self->bail_out(1)
752 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
757 for my $e (@events) {
758 my $tc = $e->{textcode};
759 next if $tc eq 'SUCCESS';
760 my $ov = "$tc.override";
761 $logger->info("circulator: attempting to override event: $ov");
763 return $self->bail_on_events($self->editor->event)
764 unless( $self->editor->allowed($ov) );
769 # --------------------------------------------------------------------------
770 # If there is an open claimsreturn circ on the requested copy, close the
771 # circ if overriding, otherwise bail out
772 # --------------------------------------------------------------------------
773 sub handle_claims_returned {
775 my $copy = $self->copy;
777 my $CR = $self->editor->search_action_circulation(
779 target_copy => $copy->id,
780 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
781 checkin_time => undef,
785 return unless ($CR = $CR->[0]);
789 # - If the caller has set the override flag, we will check the item in
790 if($self->override) {
792 $CR->checkin_time('now');
793 $CR->checkin_lib($self->editor->requestor->ws_ou);
794 $CR->checkin_staff($self->editor->requestor->id);
796 $evt = $self->editor->event
797 unless $self->editor->update_action_circulation($CR);
800 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
803 $self->bail_on_events($evt) if $evt;
808 # --------------------------------------------------------------------------
809 # This performs the checkout
810 # --------------------------------------------------------------------------
814 $self->log_me("do_checkout()");
816 # make sure perms are good if this isn't a renewal
817 unless( $self->is_renewal ) {
818 return $self->bail_on_events($self->editor->event)
819 unless( $self->editor->allowed('COPY_CHECKOUT') );
822 # verify the permit key
823 unless( $self->check_permit_key ) {
824 if( $self->permit_override ) {
825 return $self->bail_on_events($self->editor->event)
826 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
828 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
832 # if this is a non-cataloged circ, build the circ and finish
833 if( $self->is_noncat ) {
834 $self->checkout_noncat;
836 OpenILS::Event->new('SUCCESS',
837 payload => { noncat_circ => $self->circ }));
841 if( $self->is_precat ) {
842 $self->script_runner->insert("environment.isPrecat", 1, 1);
843 $self->make_precat_copy;
844 return if $self->bail_out;
846 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
847 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
850 $self->do_copy_checks;
851 return if $self->bail_out;
853 $self->run_checkout_scripts();
854 return if $self->bail_out;
856 $self->build_checkout_circ_object();
857 return if $self->bail_out;
859 $self->apply_modified_due_date();
860 return if $self->bail_out;
862 return $self->bail_on_events($self->editor->event)
863 unless $self->editor->create_action_circulation($self->circ);
865 # refresh the circ to force local time zone for now
866 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
868 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
870 return if $self->bail_out;
872 $self->handle_checkout_holds();
873 return if $self->bail_out;
875 # ------------------------------------------------------------------------------
876 # Update the patron penalty info in the DB. Run it for permit-overrides or
877 # renewals since both of those cases do not require the penalty server to
878 # run during the permit phase of the checkout
879 # ------------------------------------------------------------------------------
880 if( $self->permit_override or $self->is_renewal ) {
881 $U->update_patron_penalties(
882 authtoken => $self->editor->authtoken,
883 patron => $self->patron,
888 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
890 OpenILS::Event->new('SUCCESS',
892 copy => $U->unflesh_copy($self->copy),
895 holds_fulfilled => $self->fulfilled_holds,
903 my $copy = $self->copy;
905 my $stat = $copy->status if ref $copy->status;
906 my $loc = $copy->location if ref $copy->location;
907 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
909 $copy->status($stat->id) if $stat;
910 $copy->location($loc->id) if $loc;
911 $copy->circ_lib($circ_lib->id) if $circ_lib;
912 $copy->editor($self->editor->requestor->id);
913 $copy->edit_date('now');
914 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
916 return $self->bail_on_events($self->editor->event)
917 unless $self->editor->update_asset_copy($self->copy);
919 $copy->status($U->copy_status($copy->status));
920 $copy->location($loc) if $loc;
921 $copy->circ_lib($circ_lib) if $circ_lib;
926 my( $self, @evts ) = @_;
927 $self->push_events(@evts);
931 sub handle_checkout_holds {
934 my $copy = $self->copy;
935 my $patron = $self->patron;
937 my $holds = $self->editor->search_action_hold_request(
939 current_copy => $copy->id ,
940 cancel_time => undef,
941 fulfillment_time => undef
947 # XXX We should only fulfill one hold here...
948 # XXX If a hold was transited to the user who is checking out
949 # the item, we need to make sure that hold is what's grabbed
952 # for now, just sort by id to get what should be the oldest hold
953 $holds = [ sort { $a->id <=> $b->id } @$holds ];
954 my @myholds = grep { $_->usr eq $patron->id } @$holds;
955 my @altholds = grep { $_->usr ne $patron->id } @$holds;
958 my $hold = $myholds[0];
960 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
962 # if the hold was never officially captured, capture it.
963 $hold->capture_time('now') unless $hold->capture_time;
965 # just make sure it's set correctly
966 $hold->current_copy($copy->id);
968 $hold->fulfillment_time('now');
969 $hold->fulfillment_staff($self->editor->requestor->id);
970 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
972 return $self->bail_on_events($self->editor->event)
973 unless $self->editor->update_action_hold_request($hold);
975 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
977 push( @fulfilled, $hold->id );
980 # If there are any holds placed for other users that point to this copy,
981 # then we need to un-target those holds so the targeter can pick a new copy
984 $logger->info("circulator: un-targeting hold ".$_->id.
985 " because copy ".$copy->id." is getting checked out");
987 # - make the targeter process this hold at next run
988 $_->clear_prev_check_time;
990 # - clear out the targetted copy
991 $_->clear_current_copy;
992 $_->clear_capture_time;
994 return $self->bail_on_event($self->editor->event)
995 unless $self->editor->update_action_hold_request($_);
999 $self->fulfilled_holds(\@fulfilled);
1004 sub run_checkout_scripts {
1008 my $runner = $self->script_runner;
1009 $runner->load($self->circ_duration);
1011 my $result = $runner->run or
1012 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1014 my $duration = $result->{durationRule};
1015 my $recurring = $result->{recurringFinesRule};
1016 my $max_fine = $result->{maxFine};
1018 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1020 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1021 return $self->bail_on_events($evt) if $evt;
1023 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1024 return $self->bail_on_events($evt) if $evt;
1026 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1027 return $self->bail_on_events($evt) if $evt;
1031 # The item circulates with an unlimited duration
1037 $self->duration_rule($duration);
1038 $self->recurring_fines_rule($recurring);
1039 $self->max_fine_rule($max_fine);
1043 sub build_checkout_circ_object {
1046 my $circ = Fieldmapper::action::circulation->new;
1047 my $duration = $self->duration_rule;
1048 my $max = $self->max_fine_rule;
1049 my $recurring = $self->recurring_fines_rule;
1050 my $copy = $self->copy;
1051 my $patron = $self->patron;
1055 my $dname = $duration->name;
1056 my $mname = $max->name;
1057 my $rname = $recurring->name;
1059 $logger->debug("circulator: building circulation ".
1060 "with duration=$dname, maxfine=$mname, recurring=$rname");
1062 $circ->duration( $duration->shrt )
1063 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1064 $circ->duration( $duration->normal )
1065 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1066 $circ->duration( $duration->extended )
1067 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1069 $circ->recuring_fine( $recurring->low )
1070 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1071 $circ->recuring_fine( $recurring->normal )
1072 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1073 $circ->recuring_fine( $recurring->high )
1074 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1076 $circ->duration_rule( $duration->name );
1077 $circ->recuring_fine_rule( $recurring->name );
1078 $circ->max_fine_rule( $max->name );
1079 $circ->max_fine( $max->amount );
1081 $circ->fine_interval($recurring->recurance_interval);
1082 $circ->renewal_remaining( $duration->max_renewals );
1086 $logger->info("circulator: copy found with an unlimited circ duration");
1087 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1088 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1089 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1090 $circ->renewal_remaining(0);
1093 $circ->target_copy( $copy->id );
1094 $circ->usr( $patron->id );
1095 $circ->circ_lib( $self->circ_lib );
1097 if( $self->is_renewal ) {
1098 $circ->opac_renewal('t') if $self->opac_renewal;
1099 $circ->phone_renewal('t') if $self->phone_renewal;
1100 $circ->desk_renewal('t') if $self->desk_renewal;
1101 $circ->renewal_remaining($self->renewal_remaining);
1102 $circ->circ_staff($self->editor->requestor->id);
1106 # if the user provided an overiding checkout time,
1107 # (e.g. the checkout really happened several hours ago), then
1108 # we apply that here. Does this need a perm??
1109 $circ->xact_start(clense_ISO8601($self->checkout_time))
1110 if $self->checkout_time;
1112 # if a patron is renewing, 'requestor' will be the patron
1113 $circ->circ_staff($self->editor->requestor->id);
1114 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1120 sub apply_modified_due_date {
1122 my $circ = $self->circ;
1123 my $copy = $self->copy;
1125 if( $self->due_date ) {
1127 return $self->bail_on_events($self->editor->event)
1128 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1130 $circ->due_date(clense_ISO8601($self->due_date));
1134 # if the due_date lands on a day when the location is closed
1135 return unless $copy and $circ->due_date;
1137 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1139 # due-date overlap should be determined by the location the item
1140 # is checked out from, not the owning or circ lib of the item
1141 my $org = $self->editor->requestor->ws_ou;
1143 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1144 " with an item due date of ".$circ->due_date );
1146 my $dateinfo = $U->storagereq(
1147 'open-ils.storage.actor.org_unit.closed_date.overlap',
1148 $org, $circ->due_date );
1151 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1152 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1154 # XXX make the behavior more dynamic
1155 # for now, we just push the due date to after the close date
1156 $circ->due_date($dateinfo->{end});
1163 sub create_due_date {
1164 my( $self, $duration ) = @_;
1165 my ($sec,$min,$hour,$mday,$mon,$year) =
1166 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1167 $year += 1900; $mon += 1;
1168 my $due_date = sprintf(
1169 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1170 $year, $mon, $mday, $hour, $min, $sec);
1176 sub make_precat_copy {
1178 my $copy = $self->copy;
1181 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1183 $copy->editor($self->editor->requestor->id);
1184 $copy->edit_date('now');
1185 $copy->dummy_title($self->dummy_title);
1186 $copy->dummy_author($self->dummy_author);
1188 $self->update_copy();
1192 $logger->info("circulator: Creating a new precataloged ".
1193 "copy in checkout with barcode " . $self->copy_barcode);
1195 $copy = Fieldmapper::asset::copy->new;
1196 $copy->circ_lib($self->circ_lib);
1197 $copy->creator($self->editor->requestor->id);
1198 $copy->editor($self->editor->requestor->id);
1199 $copy->barcode($self->copy_barcode);
1200 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1201 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1202 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1204 $copy->dummy_title($self->dummy_title || "");
1205 $copy->dummy_author($self->dummy_author || "");
1207 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1209 $self->push_events($self->editor->event);
1213 # this is a little bit of a hack, but we need to
1214 # get the copy into the script runner
1215 $self->script_runner->insert("environment.copy", $copy, 1);
1219 sub checkout_noncat {
1225 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1226 my $count = $self->noncat_count || 1;
1227 my $cotime = clense_ISO8601($self->checkout_time) || "";
1229 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1233 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1234 $self->editor->requestor->id,
1242 $self->push_events($evt);
1253 $self->log_me("do_checkin()");
1256 return $self->bail_on_events(
1257 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1260 if( $self->checkin_check_holds_shelf() ) {
1261 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1262 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1263 $self->checkin_flesh_events;
1267 unless( $self->is_renewal ) {
1268 return $self->bail_on_events($self->editor->event)
1269 unless $self->editor->allowed('COPY_CHECKIN');
1272 $self->push_events($self->check_copy_alert());
1273 $self->push_events($self->check_checkin_copy_status());
1275 # the renew code will have already found our circulation object
1276 unless( $self->is_renewal and $self->circ ) {
1278 $self->editor->search_action_circulation(
1279 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1282 # if the circ is marked as 'claims returned', add the event to the list
1283 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1284 if ($self->circ and $self->circ->stop_fines
1285 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1287 # handle the overridable events
1288 $self->override_events unless $self->is_renewal;
1289 return if $self->bail_out;
1293 $self->editor->search_action_transit_copy(
1294 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1298 $self->checkin_handle_circ;
1299 return if $self->bail_out;
1300 $self->checkin_changed(1);
1302 } elsif( $self->transit ) {
1303 my $hold_transit = $self->process_received_transit;
1304 $self->checkin_changed(1);
1306 if( $self->bail_out ) {
1307 $self->checkin_flesh_events;
1311 if( my $e = $self->check_checkin_copy_status() ) {
1312 # If the original copy status is special, alert the caller
1313 my $ev = $self->events;
1314 $self->events([$e]);
1315 $self->override_events;
1316 return if $self->bail_out;
1320 if( $hold_transit or
1321 $U->copy_status($self->copy->status)->id
1322 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1325 if( $hold_transit ) {
1326 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1328 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1333 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1335 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1336 $self->reshelve_copy(1);
1337 $self->cancelled_hold_transit(1);
1338 return if $self->bail_out;
1342 # hold transited to correct location
1343 $self->checkin_flesh_events;
1348 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1350 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1351 " that is in-transit, but there is no transit.. repairing");
1352 $self->reshelve_copy(1);
1353 return if $self->bail_out;
1356 if( $self->is_renewal ) {
1357 $self->push_events(OpenILS::Event->new('SUCCESS'));
1361 # ------------------------------------------------------------------------------
1362 # Circulations and transits are now closed where necessary. Now go on to see if
1363 # this copy can fulfill a hold or needs to be routed to a different location
1364 # ------------------------------------------------------------------------------
1366 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1367 return if $self->bail_out;
1369 } else { # not needed for a hold
1371 my $circ_lib = (ref $self->copy->circ_lib) ?
1372 $self->copy->circ_lib->id : $self->copy->circ_lib;
1374 if( $self->remote_hold ) {
1375 $circ_lib = $self->remote_hold->pickup_lib;
1376 $logger->warn("circulator: Copy ".$self->copy->barcode.
1377 " is on a remote hold's shelf, sending to $circ_lib");
1380 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1382 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1384 $self->checkin_handle_precat();
1385 return if $self->bail_out;
1389 my $bc = $self->copy->barcode;
1390 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1391 $self->checkin_build_copy_transit($circ_lib);
1392 return if $self->bail_out;
1393 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1397 $self->reshelve_copy;
1398 return if $self->bail_out;
1400 unless($self->checkin_changed) {
1402 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1403 my $stat = $U->copy_status($self->copy->status)->id;
1405 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1406 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1407 $self->bail_out(1); # no need to commit anything
1411 $self->push_events(OpenILS::Event->new('SUCCESS'))
1412 unless @{$self->events};
1416 # ------------------------------------------------------------------------------
1417 # Update the patron penalty info in the DB
1418 # ------------------------------------------------------------------------------
1419 $U->update_patron_penalties(
1420 authtoken => $self->editor->authtoken,
1421 patron => $self->patron,
1422 background => 1 ) if $self->is_checkin;
1424 $self->checkin_flesh_events;
1430 my $force = $self->force || shift;
1431 my $copy = $self->copy;
1433 my $stat = $U->copy_status($copy->status)->id;
1436 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1437 $stat != OILS_COPY_STATUS_CATALOGING and
1438 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1439 $stat != OILS_COPY_STATUS_RESHELVING )) {
1441 $copy->status( OILS_COPY_STATUS_RESHELVING );
1443 $self->checkin_changed(1);
1448 # Returns true if the item is at the current location
1449 # because it was transited there for a hold and the
1450 # hold has not been fulfilled
1451 sub checkin_check_holds_shelf {
1453 return 0 unless $self->copy;
1456 $U->copy_status($self->copy->status)->id ==
1457 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1459 # find the hold that put us on the holds shelf
1460 my $holds = $self->editor->search_action_hold_request(
1462 current_copy => $self->copy->id,
1463 capture_time => { '!=' => undef },
1464 fulfillment_time => undef,
1465 cancel_time => undef,
1470 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1471 $self->reshelve_copy(1);
1475 my $hold = $$holds[0];
1477 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1478 $hold->id. "] for copy ".$self->copy->barcode);
1480 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1481 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1485 $logger->info("circulator: hold is not for here..");
1486 $self->remote_hold($hold);
1491 sub checkin_handle_precat {
1493 my $copy = $self->copy;
1495 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1496 $copy->status(OILS_COPY_STATUS_CATALOGING);
1497 $self->update_copy();
1498 $self->checkin_changed(1);
1499 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1504 sub checkin_build_copy_transit {
1507 my $copy = $self->copy;
1508 my $transit = Fieldmapper::action::transit_copy->new;
1510 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1511 $logger->info("circulator: transiting copy to $dest");
1513 $transit->source($self->editor->requestor->ws_ou);
1514 $transit->dest($dest);
1515 $transit->target_copy($copy->id);
1516 $transit->source_send_time('now');
1517 $transit->copy_status( $U->copy_status($copy->status)->id );
1519 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1521 return $self->bail_on_events($self->editor->event)
1522 unless $self->editor->create_action_transit_copy($transit);
1524 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1526 $self->checkin_changed(1);
1530 sub attempt_checkin_hold_capture {
1532 my $copy = $self->copy;
1534 # See if this copy can fulfill any holds
1535 my ($hold) = $holdcode->find_nearest_permitted_hold(
1536 $self->editor, $copy, $self->editor->requestor );
1539 $logger->debug("circulator: no potential permitted".
1540 "holds found for copy ".$copy->barcode);
1545 $logger->info("circulator: found permitted hold ".
1546 $hold->id . " for copy, capturing...");
1548 $hold->current_copy($copy->id);
1549 $hold->capture_time('now');
1551 # prevent DB errors caused by fetching
1552 # holds from storage, and updating through cstore
1553 $hold->clear_fulfillment_time;
1554 $hold->clear_fulfillment_staff;
1555 $hold->clear_fulfillment_lib;
1556 $hold->clear_expire_time;
1557 $hold->clear_cancel_time;
1558 $hold->clear_prev_check_time unless $hold->prev_check_time;
1560 $self->bail_on_events($self->editor->event)
1561 unless $self->editor->update_action_hold_request($hold);
1563 $self->checkin_changed(1);
1565 return 1 if $self->bail_out;
1567 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1569 # This hold was captured in the correct location
1570 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1571 $self->push_events(OpenILS::Event->new('SUCCESS'));
1573 #$self->do_hold_notify($hold->id);
1574 $self->notify_hold($hold->id);
1578 # Hold needs to be picked up elsewhere. Build a hold
1579 # transit and route the item.
1580 $self->checkin_build_hold_transit();
1581 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1582 return 1 if $self->bail_out;
1584 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1587 # make sure we save the copy status
1592 sub do_hold_notify {
1593 my( $self, $holdid ) = @_;
1595 $logger->info("circulator: running delayed hold notify process");
1597 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1598 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1600 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1601 hold_id => $holdid, requestor => $self->editor->requestor);
1603 $logger->debug("circulator: built hold notifier");
1605 if(!$notifier->event) {
1607 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1609 my $stat = $notifier->send_email_notify;
1610 if( $stat == '1' ) {
1611 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1615 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1618 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1623 sub checkin_build_hold_transit {
1626 my $copy = $self->copy;
1627 my $hold = $self->hold;
1628 my $trans = Fieldmapper::action::hold_transit_copy->new;
1630 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1632 $trans->hold($hold->id);
1633 $trans->source($self->editor->requestor->ws_ou);
1634 $trans->dest($hold->pickup_lib);
1635 $trans->source_send_time("now");
1636 $trans->target_copy($copy->id);
1638 # when the copy gets to its destination, it will recover
1639 # this status - put it onto the holds shelf
1640 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1642 return $self->bail_on_events($self->editor->event)
1643 unless $self->editor->create_action_hold_transit_copy($trans);
1648 sub process_received_transit {
1650 my $copy = $self->copy;
1651 my $copyid = $self->copy->id;
1653 my $status_name = $U->copy_status($copy->status)->name;
1654 $logger->debug("circulator: attempting transit receive on ".
1655 "copy $copyid. Copy status is $status_name");
1657 my $transit = $self->transit;
1659 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1660 my $tid = $transit->id;
1661 $logger->info("circulator: Fowarding transit on copy which is destined ".
1662 "for a different location. transit=$tid, copy=$copyid,current ".
1663 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1665 return $self->bail_on_events(
1666 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1669 # The transit is received, set the receive time
1670 $transit->dest_recv_time('now');
1671 $self->bail_on_events($self->editor->event)
1672 unless $self->editor->update_action_transit_copy($transit);
1674 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1676 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1677 $copy->status( $transit->copy_status );
1678 $self->update_copy();
1679 return if $self->bail_out;
1683 #$self->do_hold_notify($hold_transit->hold);
1684 $self->notify_hold($hold_transit->hold);
1689 OpenILS::Event->new(
1692 payload => { transit => $transit, holdtransit => $hold_transit } ));
1694 return $hold_transit;
1698 sub checkin_handle_circ {
1702 my $circ = $self->circ;
1703 my $copy = $self->copy;
1707 # backdate the circ if necessary
1708 if($self->backdate) {
1709 $self->checkin_handle_backdate;
1710 return if $self->bail_out;
1713 if(!$circ->stop_fines) {
1714 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1715 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1716 $circ->stop_fines_time('now') unless $self->backdate;
1717 $circ->stop_fines_time($self->backdate) if $self->backdate;
1720 # see if there are any fines owed on this circ. if not, close it
1721 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1722 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1724 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1726 # Set the checkin vars since we have the item
1727 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1729 $circ->checkin_staff($self->editor->requestor->id);
1730 $circ->checkin_lib($self->editor->requestor->ws_ou);
1732 my $circ_lib = (ref $self->copy->circ_lib) ?
1733 $self->copy->circ_lib->id : $self->copy->circ_lib;
1734 my $stat = $U->copy_status($self->copy->status)->id;
1736 # If the item is lost/missing and it needs to be sent home, don't
1737 # reshelve the copy, leave it lost/missing so the recipient will know
1738 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1739 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1740 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1743 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1747 return $self->bail_on_events($self->editor->event)
1748 unless $self->editor->update_action_circulation($circ);
1752 sub checkin_handle_backdate {
1755 my $bd = $self->backdate;
1757 # ------------------------------------------------------------------
1758 # clean up the backdate for date comparison
1759 # we want any bills created on or after the backdate
1760 # ------------------------------------------------------------------
1761 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1762 #$bd = "${bd}T23:59:59";
1764 my $bills = $self->editor->search_money_billing(
1766 billing_ts => { '>=' => $bd },
1767 xact => $self->circ->id,
1768 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1772 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1774 for my $bill (@$bills) {
1775 unless( $U->is_true($bill->voided) ) {
1776 $logger->info("backdate voiding bill ".$bill->id);
1778 $bill->void_time('now');
1779 $bill->voider($self->editor->requestor->id);
1780 my $n = $bill->note || "";
1781 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1783 $self->bail_on_events($self->editor->event)
1784 unless $self->editor->update_money_billing($bill);
1792 # XXX Legacy version for Circ.pm support
1793 sub _checkin_handle_backdate {
1794 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1797 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1798 $bd = "${bd}T23:59:59";
1800 my $bills = $session->request(
1801 "open-ils.storage.direct.money.billing.search_where.atomic",
1802 billing_ts => { '>=' => $bd },
1804 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1807 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1810 for my $bill (@$bills) {
1811 unless( $U->is_true($bill->voided) ) {
1812 $logger->debug("voiding bill ".$bill->id);
1814 $bill->void_time('now');
1815 $bill->voider($requestor->id);
1816 my $n = $bill->note || "";
1817 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1818 my $s = $session->request(
1819 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1820 return $U->DB_UPDATE_FAILED($bill) unless $s;
1834 sub find_patron_from_copy {
1836 my $circs = $self->editor->search_action_circulation(
1837 { target_copy => $self->copy->id, checkin_time => undef });
1838 my $circ = $circs->[0];
1839 return unless $circ;
1840 my $u = $self->editor->retrieve_actor_user($circ->usr)
1841 or return $self->bail_on_events($self->editor->event);
1845 sub check_checkin_copy_status {
1847 my $copy = $self->copy;
1853 my $status = $U->copy_status($copy->status)->id;
1856 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1857 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1858 $status == OILS_COPY_STATUS_IN_PROCESS ||
1859 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1860 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1861 $status == OILS_COPY_STATUS_CATALOGING ||
1862 $status == OILS_COPY_STATUS_RESHELVING );
1864 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1865 if( $status == OILS_COPY_STATUS_LOST );
1867 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1868 if( $status == OILS_COPY_STATUS_MISSING );
1870 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1875 # --------------------------------------------------------------------------
1876 # On checkin, we need to return as many relevant objects as we can
1877 # --------------------------------------------------------------------------
1878 sub checkin_flesh_events {
1881 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1882 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1883 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1887 for my $evt (@{$self->events}) {
1890 $payload->{copy} = $U->unflesh_copy($self->copy);
1891 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1892 $payload->{circ} = $self->circ;
1893 $payload->{transit} = $self->transit;
1894 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1896 # $self->hold may or may not have been replaced with a
1897 # valid hold after processing a cancelled hold
1898 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1900 $evt->{payload} = $payload;
1905 my( $self, $msg ) = @_;
1906 my $bc = ($self->copy) ? $self->copy->barcode :
1909 my $usr = ($self->patron) ? $self->patron->id : "";
1910 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1911 ", recipient=$usr, copy=$bc");
1917 $self->log_me("do_renew()");
1918 $self->is_renewal(1);
1920 unless( $self->is_renewal ) {
1921 return $self->bail_on_events($self->editor->events)
1922 unless $self->editor->allowed('RENEW_CIRC');
1925 # Make sure there is an open circ to renew that is not
1926 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1927 my $circ = $self->editor->search_action_circulation(
1928 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1931 $circ = $self->editor->search_action_circulation(
1933 target_copy => $self->copy->id,
1934 stop_fines => OILS_STOP_FINES_MAX_FINES,
1935 checkin_time => undef
1940 return $self->bail_on_events($self->editor->event) unless $circ;
1942 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1943 if $circ->renewal_remaining < 1;
1945 # -----------------------------------------------------------------
1947 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1950 $self->run_renew_permit;
1953 $self->do_checkin();
1954 return if $self->bail_out;
1956 unless( $self->permit_override ) {
1958 return if $self->bail_out;
1959 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1960 $self->remove_event('ITEM_NOT_CATALOGED');
1963 $self->override_events;
1964 return if $self->bail_out;
1967 $self->do_checkout();
1972 my( $self, $evt ) = @_;
1973 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1974 $logger->debug("circulator: removing event from list: $evt");
1975 my @events = @{$self->events};
1976 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1981 my( $self, $evt ) = @_;
1982 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1983 return grep { $_->{textcode} eq $evt } @{$self->events};
1988 sub run_renew_permit {
1990 my $runner = $self->script_runner;
1992 $runner->load($self->circ_permit_renew);
1993 my $result = $runner->run or
1994 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1995 my $events = $result->{events};
1997 $logger->activity("ciculator: circ_permit_renew for user ".
1998 $self->patron->id." returned events: @$events") if @$events;
2000 $self->push_events(OpenILS::Event->new($_)) for @$events;
2002 $logger->debug("circulator: re-creating script runner to be safe");
2003 $self->mk_script_runner;