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 ) {
1324 my $hold = ($hold_transit) ?
1325 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1326 $U->fetch_open_hold_by_copy($self->copy->id);
1330 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1332 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1333 $self->reshelve_copy(1);
1334 $self->cancelled_hold_transit(1);
1335 return if $self->bail_out;
1339 # hold transited to correct location
1340 $self->checkin_flesh_events;
1345 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1347 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1348 " that is in-transit, but there is no transit.. repairing");
1349 $self->reshelve_copy(1);
1350 return if $self->bail_out;
1353 if( $self->is_renewal ) {
1354 $self->push_events(OpenILS::Event->new('SUCCESS'));
1358 # ------------------------------------------------------------------------------
1359 # Circulations and transits are now closed where necessary. Now go on to see if
1360 # this copy can fulfill a hold or needs to be routed to a different location
1361 # ------------------------------------------------------------------------------
1363 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1364 return if $self->bail_out;
1366 } else { # not needed for a hold
1368 my $circ_lib = (ref $self->copy->circ_lib) ?
1369 $self->copy->circ_lib->id : $self->copy->circ_lib;
1371 if( $self->remote_hold ) {
1372 $circ_lib = $self->remote_hold->pickup_lib;
1373 $logger->warn("circulator: Copy ".$self->copy->barcode.
1374 " is on a remote hold's shelf, sending to $circ_lib");
1377 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1379 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1381 $self->checkin_handle_precat();
1382 return if $self->bail_out;
1386 my $bc = $self->copy->barcode;
1387 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1388 $self->checkin_build_copy_transit($circ_lib);
1389 return if $self->bail_out;
1390 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1394 $self->reshelve_copy;
1395 return if $self->bail_out;
1397 unless($self->checkin_changed) {
1399 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1400 my $stat = $U->copy_status($self->copy->status)->id;
1402 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1403 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1404 $self->bail_out(1); # no need to commit anything
1408 $self->push_events(OpenILS::Event->new('SUCCESS'))
1409 unless @{$self->events};
1413 # ------------------------------------------------------------------------------
1414 # Update the patron penalty info in the DB
1415 # ------------------------------------------------------------------------------
1416 $U->update_patron_penalties(
1417 authtoken => $self->editor->authtoken,
1418 patron => $self->patron,
1419 background => 1 ) if $self->is_checkin;
1421 $self->checkin_flesh_events;
1427 my $force = $self->force || shift;
1428 my $copy = $self->copy;
1430 my $stat = $U->copy_status($copy->status)->id;
1433 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1434 $stat != OILS_COPY_STATUS_CATALOGING and
1435 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1436 $stat != OILS_COPY_STATUS_RESHELVING )) {
1438 $copy->status( OILS_COPY_STATUS_RESHELVING );
1440 $self->checkin_changed(1);
1445 # Returns true if the item is at the current location
1446 # because it was transited there for a hold and the
1447 # hold has not been fulfilled
1448 sub checkin_check_holds_shelf {
1450 return 0 unless $self->copy;
1453 $U->copy_status($self->copy->status)->id ==
1454 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1456 # find the hold that put us on the holds shelf
1457 my $holds = $self->editor->search_action_hold_request(
1459 current_copy => $self->copy->id,
1460 capture_time => { '!=' => undef },
1461 fulfillment_time => undef,
1462 cancel_time => undef,
1467 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1468 $self->reshelve_copy(1);
1472 my $hold = $$holds[0];
1474 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1475 $hold->id. "] for copy ".$self->copy->barcode);
1477 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1478 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1482 $logger->info("circulator: hold is not for here..");
1483 $self->remote_hold($hold);
1488 sub checkin_handle_precat {
1490 my $copy = $self->copy;
1492 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1493 $copy->status(OILS_COPY_STATUS_CATALOGING);
1494 $self->update_copy();
1495 $self->checkin_changed(1);
1496 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1501 sub checkin_build_copy_transit {
1504 my $copy = $self->copy;
1505 my $transit = Fieldmapper::action::transit_copy->new;
1507 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1508 $logger->info("circulator: transiting copy to $dest");
1510 $transit->source($self->editor->requestor->ws_ou);
1511 $transit->dest($dest);
1512 $transit->target_copy($copy->id);
1513 $transit->source_send_time('now');
1514 $transit->copy_status( $U->copy_status($copy->status)->id );
1516 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1518 return $self->bail_on_events($self->editor->event)
1519 unless $self->editor->create_action_transit_copy($transit);
1521 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1523 $self->checkin_changed(1);
1527 sub attempt_checkin_hold_capture {
1529 my $copy = $self->copy;
1531 # See if this copy can fulfill any holds
1532 my ($hold) = $holdcode->find_nearest_permitted_hold(
1533 OpenSRF::AppSession->create('open-ils.storage'),
1534 $copy, $self->editor->requestor );
1537 $logger->debug("circulator: no potential permitted".
1538 "holds found for copy ".$copy->barcode);
1543 $logger->info("circulator: found permitted hold ".
1544 $hold->id . " for copy, capturing...");
1546 $hold->current_copy($copy->id);
1547 $hold->capture_time('now');
1549 # prevent DB errors caused by fetching
1550 # holds from storage, and updating through cstore
1551 $hold->clear_fulfillment_time;
1552 $hold->clear_fulfillment_staff;
1553 $hold->clear_fulfillment_lib;
1554 $hold->clear_expire_time;
1555 $hold->clear_cancel_time;
1556 $hold->clear_prev_check_time unless $hold->prev_check_time;
1558 $self->bail_on_events($self->editor->event)
1559 unless $self->editor->update_action_hold_request($hold);
1561 $self->checkin_changed(1);
1563 return 1 if $self->bail_out;
1565 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1567 # This hold was captured in the correct location
1568 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1569 $self->push_events(OpenILS::Event->new('SUCCESS'));
1571 #$self->do_hold_notify($hold->id);
1572 $self->notify_hold($hold->id);
1576 # Hold needs to be picked up elsewhere. Build a hold
1577 # transit and route the item.
1578 $self->checkin_build_hold_transit();
1579 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1580 return 1 if $self->bail_out;
1582 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1585 # make sure we save the copy status
1590 sub do_hold_notify {
1591 my( $self, $holdid ) = @_;
1593 $logger->info("circulator: running delayed hold notify process");
1595 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1596 hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1598 $logger->debug("circulator: built hold notifier");
1600 if(!$notifier->event) {
1602 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1604 my $stat = $notifier->send_email_notify;
1605 if( $stat == '1' ) {
1606 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1610 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1613 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1618 sub checkin_build_hold_transit {
1621 my $copy = $self->copy;
1622 my $hold = $self->hold;
1623 my $trans = Fieldmapper::action::hold_transit_copy->new;
1625 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1627 $trans->hold($hold->id);
1628 $trans->source($self->editor->requestor->ws_ou);
1629 $trans->dest($hold->pickup_lib);
1630 $trans->source_send_time("now");
1631 $trans->target_copy($copy->id);
1633 # when the copy gets to its destination, it will recover
1634 # this status - put it onto the holds shelf
1635 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1637 return $self->bail_on_events($self->editor->event)
1638 unless $self->editor->create_action_hold_transit_copy($trans);
1643 sub process_received_transit {
1645 my $copy = $self->copy;
1646 my $copyid = $self->copy->id;
1648 my $status_name = $U->copy_status($copy->status)->name;
1649 $logger->debug("circulator: attempting transit receive on ".
1650 "copy $copyid. Copy status is $status_name");
1652 my $transit = $self->transit;
1654 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1655 my $tid = $transit->id;
1656 $logger->info("circulator: Fowarding transit on copy which is destined ".
1657 "for a different location. transit=$tid, copy=$copyid,current ".
1658 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1660 return $self->bail_on_events(
1661 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1664 # The transit is received, set the receive time
1665 $transit->dest_recv_time('now');
1666 $self->bail_on_events($self->editor->event)
1667 unless $self->editor->update_action_transit_copy($transit);
1669 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1671 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1672 $copy->status( $transit->copy_status );
1673 $self->update_copy();
1674 return if $self->bail_out;
1678 #$self->do_hold_notify($hold_transit->hold);
1679 $self->notify_hold($hold_transit->hold);
1684 OpenILS::Event->new(
1687 payload => { transit => $transit, holdtransit => $hold_transit } ));
1689 return $hold_transit;
1693 sub checkin_handle_circ {
1697 my $circ = $self->circ;
1698 my $copy = $self->copy;
1702 # backdate the circ if necessary
1703 if($self->backdate) {
1704 $self->checkin_handle_backdate;
1705 return if $self->bail_out;
1708 if(!$circ->stop_fines) {
1709 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1710 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1711 $circ->stop_fines_time('now') unless $self->backdate;
1712 $circ->stop_fines_time($self->backdate) if $self->backdate;
1715 # see if there are any fines owed on this circ. if not, close it
1716 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1717 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1719 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1721 # Set the checkin vars since we have the item
1722 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1724 $circ->checkin_staff($self->editor->requestor->id);
1725 $circ->checkin_lib($self->editor->requestor->ws_ou);
1727 my $circ_lib = (ref $self->copy->circ_lib) ?
1728 $self->copy->circ_lib->id : $self->copy->circ_lib;
1729 my $stat = $U->copy_status($self->copy->status)->id;
1731 # If the item is lost/missing and it needs to be sent home, don't
1732 # reshelve the copy, leave it lost/missing so the recipient will know
1733 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1734 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1735 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1738 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1742 return $self->bail_on_events($self->editor->event)
1743 unless $self->editor->update_action_circulation($circ);
1747 sub checkin_handle_backdate {
1750 my $bd = $self->backdate;
1752 # ------------------------------------------------------------------
1753 # clean up the backdate for date comparison
1754 # we want any bills created on or after the backdate
1755 # ------------------------------------------------------------------
1756 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1757 #$bd = "${bd}T23:59:59";
1759 my $bills = $self->editor->search_money_billing(
1761 billing_ts => { '>=' => $bd },
1762 xact => $self->circ->id,
1763 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1767 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1769 for my $bill (@$bills) {
1770 unless( $U->is_true($bill->voided) ) {
1771 $logger->info("backdate voiding bill ".$bill->id);
1773 $bill->void_time('now');
1774 $bill->voider($self->editor->requestor->id);
1775 my $n = $bill->note || "";
1776 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1778 $self->bail_on_events($self->editor->event)
1779 unless $self->editor->update_money_billing($bill);
1787 # XXX Legacy version for Circ.pm support
1788 sub _checkin_handle_backdate {
1789 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1792 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1793 $bd = "${bd}T23:59:59";
1795 my $bills = $session->request(
1796 "open-ils.storage.direct.money.billing.search_where.atomic",
1797 billing_ts => { '>=' => $bd },
1799 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1802 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1805 for my $bill (@$bills) {
1806 unless( $U->is_true($bill->voided) ) {
1807 $logger->debug("voiding bill ".$bill->id);
1809 $bill->void_time('now');
1810 $bill->voider($requestor->id);
1811 my $n = $bill->note || "";
1812 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1813 my $s = $session->request(
1814 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1815 return $U->DB_UPDATE_FAILED($bill) unless $s;
1829 sub find_patron_from_copy {
1831 my $circs = $self->editor->search_action_circulation(
1832 { target_copy => $self->copy->id, checkin_time => undef });
1833 my $circ = $circs->[0];
1834 return unless $circ;
1835 my $u = $self->editor->retrieve_actor_user($circ->usr)
1836 or return $self->bail_on_events($self->editor->event);
1840 sub check_checkin_copy_status {
1842 my $copy = $self->copy;
1848 my $status = $U->copy_status($copy->status)->id;
1851 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1852 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1853 $status == OILS_COPY_STATUS_IN_PROCESS ||
1854 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1855 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1856 $status == OILS_COPY_STATUS_CATALOGING ||
1857 $status == OILS_COPY_STATUS_RESHELVING );
1859 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1860 if( $status == OILS_COPY_STATUS_LOST );
1862 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1863 if( $status == OILS_COPY_STATUS_MISSING );
1865 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1870 # --------------------------------------------------------------------------
1871 # On checkin, we need to return as many relevant objects as we can
1872 # --------------------------------------------------------------------------
1873 sub checkin_flesh_events {
1876 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1877 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1878 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1882 for my $evt (@{$self->events}) {
1885 $payload->{copy} = $U->unflesh_copy($self->copy);
1886 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1887 $payload->{circ} = $self->circ;
1888 $payload->{transit} = $self->transit;
1889 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1891 # $self->hold may or may not have been replaced with a
1892 # valid hold after processing a cancelled hold
1893 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1895 $evt->{payload} = $payload;
1900 my( $self, $msg ) = @_;
1901 my $bc = ($self->copy) ? $self->copy->barcode :
1904 my $usr = ($self->patron) ? $self->patron->id : "";
1905 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1906 ", recipient=$usr, copy=$bc");
1912 $self->log_me("do_renew()");
1913 $self->is_renewal(1);
1915 unless( $self->is_renewal ) {
1916 return $self->bail_on_events($self->editor->events)
1917 unless $self->editor->allowed('RENEW_CIRC');
1920 # Make sure there is an open circ to renew that is not
1921 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1922 my $circ = $self->editor->search_action_circulation(
1923 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1926 $circ = $self->editor->search_action_circulation(
1928 target_copy => $self->copy->id,
1929 stop_fines => OILS_STOP_FINES_MAX_FINES,
1930 checkin_time => undef
1935 return $self->bail_on_events($self->editor->event) unless $circ;
1937 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1938 if $circ->renewal_remaining < 1;
1940 # -----------------------------------------------------------------
1942 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1945 $self->run_renew_permit;
1948 $self->do_checkin();
1949 return if $self->bail_out;
1951 unless( $self->permit_override ) {
1953 return if $self->bail_out;
1954 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1955 $self->remove_event('ITEM_NOT_CATALOGED');
1958 $self->override_events;
1959 return if $self->bail_out;
1962 $self->do_checkout();
1967 my( $self, $evt ) = @_;
1968 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1969 $logger->debug("circulator: removing event from list: $evt");
1970 my @events = @{$self->events};
1971 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1976 my( $self, $evt ) = @_;
1977 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1978 return grep { $_->{textcode} eq $evt } @{$self->events};
1983 sub run_renew_permit {
1985 my $runner = $self->script_runner;
1987 $runner->load($self->circ_permit_renew);
1988 my $result = $runner->run or
1989 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1990 my $events = $result->{events};
1992 $logger->activity("ciculator: circ_permit_renew for user ".
1993 $self->patron->id." returned events: @$events") if @$events;
1995 $self->push_events(OpenILS::Event->new($_)) for @$events;
1997 $logger->debug("circulator: re-creating script runner to be safe");
1998 $self->mk_script_runner;