1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenSRF::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
15 my $conf = OpenSRF::Utils::SettingsClient->new;
16 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
17 my @pfx = ( @pfx2, "scripts" );
19 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
20 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
21 my $d = $conf->config_value( @pfx, 'circ_duration' );
22 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
23 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
24 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
25 my $lb = $conf->config_value( @pfx2, 'script_path' );
27 $logger->error( "Missing circ script(s)" )
28 unless( $p and $c and $d and $f and $m and $pr );
30 $scripts{circ_permit_patron} = $p;
31 $scripts{circ_permit_copy} = $c;
32 $scripts{circ_duration} = $d;
33 $scripts{circ_recurring_fines}= $f;
34 $scripts{circ_max_fines} = $m;
35 $scripts{circ_permit_renew} = $pr;
37 $lb = [ $lb ] unless ref($lb);
41 "circulator: Loaded rules scripts for circ: " .
42 "circ permit patron = $p, ".
43 "circ permit copy = $c, ".
44 "circ duration = $d, ".
45 "circ recurring fines = $f, " .
46 "circ max fines = $m, ".
47 "circ renew permit = $pr. ".
52 __PACKAGE__->register_method(
53 method => "run_method",
54 api_name => "open-ils.circ.checkout.permit",
56 Determines if the given checkout can occur
57 @param authtoken The login session key
58 @param params A trailing hash of named params including
59 barcode : The copy barcode,
60 patron : The patron the checkout is occurring for,
61 renew : true or false - whether or not this is a renewal
62 @return The event that occurred during the permit check.
66 __PACKAGE__->register_method (
67 method => 'run_method',
68 api_name => 'open-ils.circ.checkout.permit.override',
69 signature => q/@see open-ils.circ.checkout.permit/,
73 __PACKAGE__->register_method(
74 method => "run_method",
75 api_name => "open-ils.circ.checkout",
78 @param authtoken The login session key
79 @param params A named hash of params including:
81 barcode If no copy is provided, the copy is retrieved via barcode
82 copyid If no copy or barcode is provide, the copy id will be use
83 patron The patron's id
84 noncat True if this is a circulation for a non-cataloted item
85 noncat_type The non-cataloged type id
86 noncat_circ_lib The location for the noncat circ.
87 precat The item has yet to be cataloged
88 dummy_title The temporary title of the pre-cataloded item
89 dummy_author The temporary authr of the pre-cataloded item
90 Default is the home org of the staff member
91 @return The SUCCESS event on success, any other event depending on the error
94 __PACKAGE__->register_method(
95 method => "run_method",
96 api_name => "open-ils.circ.checkin",
99 Generic super-method for handling all copies
100 @param authtoken The login session key
101 @param params Hash of named parameters including:
102 barcode - The copy barcode
103 force - If true, copies in bad statuses will be checked in and give good statuses
108 __PACKAGE__->register_method(
109 method => "run_method",
110 api_name => "open-ils.circ.checkin.override",
111 signature => q/@see open-ils.circ.checkin/
114 __PACKAGE__->register_method(
115 method => "run_method",
116 api_name => "open-ils.circ.renew.override",
117 signature => q/@see open-ils.circ.renew/,
121 __PACKAGE__->register_method(
122 method => "run_method",
123 api_name => "open-ils.circ.renew",
124 notes => <<" NOTES");
125 PARAMS( authtoken, circ => circ_id );
126 open-ils.circ.renew(login_session, circ_object);
127 Renews the provided circulation. login_session is the requestor of the
128 renewal and if the logged in user is not the same as circ->usr, then
129 the logged in user must have RENEW_CIRC permissions.
132 __PACKAGE__->register_method(
133 method => "run_method",
134 api_name => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136 method => "run_method",
137 api_name => "open-ils.circ.checkout.full.override");
142 my( $self, $conn, $auth, $args ) = @_;
143 translate_legacy_args($args);
144 my $api = $self->api_name;
147 OpenILS::Application::Circ::Circulator->new($auth, %$args);
149 return circ_events($circulator) if $circulator->bail_out;
151 # --------------------------------------------------------------------------
152 # Go ahead and load the script runner to make sure we have all
153 # of the objects we need
154 # --------------------------------------------------------------------------
155 $circulator->is_renewal(1) if $api =~ /renew/;
156 $circulator->is_checkin(1) if $api =~ /checkin/;
157 $circulator->mk_script_runner;
158 return circ_events($circulator) if $circulator->bail_out;
160 $circulator->circ_permit_patron($scripts{circ_permit_patron});
161 $circulator->circ_permit_copy($scripts{circ_permit_copy});
162 $circulator->circ_duration($scripts{circ_duration});
163 $circulator->circ_permit_renew($scripts{circ_permit_renew});
165 $circulator->override(1) if $api =~ /override/o;
167 if( $api =~ /checkout\.permit/ ) {
168 $circulator->do_permit();
170 } elsif( $api =~ /checkout.full/ ) {
172 $circulator->do_permit();
173 unless( $circulator->bail_out ) {
174 $circulator->events([]);
175 $circulator->do_checkout();
178 } elsif( $api =~ /checkout/ ) {
179 $circulator->do_checkout();
181 } elsif( $api =~ /checkin/ ) {
182 $circulator->do_checkin();
184 } elsif( $api =~ /renew/ ) {
185 $circulator->is_renewal(1);
186 $circulator->do_renew();
189 if( $circulator->bail_out ) {
192 # make sure no success event accidentally slip in
194 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
195 my @e = @{$circulator->events};
196 push( @ee, $_->{textcode} ) for @e;
197 $logger->info("circulator: bailing out with events: @ee");
198 $circulator->editor->rollback;
201 $circulator->editor->commit;
204 $circulator->script_runner->cleanup;
206 $conn->respond_complete(circ_events($circulator));
208 unless($circulator->bail_out) {
209 $circulator->do_hold_notify($circulator->notify_hold)
210 if $circulator->notify_hold;
216 my @e = @{$circ->events};
217 return (@e == 1) ? $e[0] : \@e;
222 sub translate_legacy_args {
225 if( $$args{barcode} ) {
226 $$args{copy_barcode} = $$args{barcode};
227 delete $$args{barcode};
230 if( $$args{copyid} ) {
231 $$args{copy_id} = $$args{copyid};
232 delete $$args{copyid};
235 if( $$args{patronid} ) {
236 $$args{patron_id} = $$args{patronid};
237 delete $$args{patronid};
240 if( $$args{patron} and !ref($$args{patron}) ) {
241 $$args{patron_id} = $$args{patron};
242 delete $$args{patron};
246 if( $$args{noncat} ) {
247 $$args{is_noncat} = $$args{noncat};
248 delete $$args{noncat};
251 if( $$args{precat} ) {
252 $$args{is_precat} = $$args{precat};
253 delete $$args{precat};
259 # --------------------------------------------------------------------------
260 # This package actually manages all of the circulation logic
261 # --------------------------------------------------------------------------
262 package OpenILS::Application::Circ::Circulator;
263 use strict; use warnings;
264 use vars q/$AUTOLOAD/;
266 use OpenILS::Utils::Fieldmapper;
267 use OpenSRF::Utils::Cache;
268 use Digest::MD5 qw(md5_hex);
269 use DateTime::Format::ISO8601;
270 use OpenILS::Utils::PermitHold;
271 use OpenSRF::Utils qw/:datetime/;
272 use OpenSRF::Utils::SettingsClient;
273 use OpenILS::Application::Circ::Holds;
274 use OpenILS::Application::Circ::Transit;
275 use OpenSRF::Utils::Logger qw(:logger);
276 use OpenILS::Utils::CStoreEditor qw/:funcs/;
277 use OpenILS::Application::Circ::ScriptBuilder;
278 use OpenILS::Const qw/:const/;
280 my $U = "OpenILS::Application::AppUtils";
281 my $holdcode = "OpenILS::Application::Circ::Holds";
282 my $transcode = "OpenILS::Application::Circ::Transit";
287 # --------------------------------------------------------------------------
288 # Add a pile of automagic getter/setter methods
289 # --------------------------------------------------------------------------
290 my @AUTOLOAD_FIELDS = qw/
331 recurring_fines_level
349 my $type = ref($self) or die "$self is not an object";
351 my $name = $AUTOLOAD;
354 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
355 $logger->error("circulator: $type: invalid autoload field: $name");
356 die "$type: invalid autoload field: $name\n"
361 *{"${type}::${name}"} = sub {
364 $s->{$name} = $v if defined $v;
368 return $self->$name($data);
373 my( $class, $auth, %args ) = @_;
374 $class = ref($class) || $class;
375 my $self = bless( {}, $class );
379 new_editor(xact => 1, authtoken => $auth) );
381 unless( $self->editor->checkauth ) {
382 $self->bail_on_events($self->editor->event);
386 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
388 $self->$_($args{$_}) for keys %args;
391 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
397 # --------------------------------------------------------------------------
398 # True if we should discontinue processing
399 # --------------------------------------------------------------------------
401 my( $self, $bool ) = @_;
402 if( defined $bool ) {
403 $logger->info("circulator: BAILING OUT") if $bool;
404 $self->{bail_out} = $bool;
406 return $self->{bail_out};
411 my( $self, @evts ) = @_;
414 $logger->info("circulator: pushing event ".$e->{textcode});
415 push( @{$self->events}, $e ) unless
416 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
422 my $key = md5_hex( time() . rand() . "$$" );
423 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
424 return $self->permit_key($key);
427 sub check_permit_key {
429 my $key = $self->permit_key;
430 return 0 unless $key;
431 my $k = "oils_permit_key_$key";
432 my $one = $self->cache_handle->get_cache($k);
433 $self->cache_handle->delete_cache($k);
434 return ($one) ? 1 : 0;
438 # --------------------------------------------------------------------------
439 # This builds the script runner environment and fetches most of the
441 # --------------------------------------------------------------------------
442 sub mk_script_runner {
448 qw/copy copy_barcode copy_id patron
449 patron_id patron_barcode volume title editor/;
451 # Translate our objects into the ScriptBuilder args hash
452 $$args{$_} = $self->$_() for @fields;
454 $args->{ignore_user_status} = 1 if $self->is_checkin;
455 $$args{fetch_patron_by_circ_copy} = 1;
456 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
458 if( my $pco = $self->pending_checkouts ) {
459 $logger->info("circulator: we were given a pending checkouts number of $pco");
460 $$args{patronItemsOut} = $pco;
463 # This fetches most of the objects we need
464 $self->script_runner(
465 OpenILS::Application::Circ::ScriptBuilder->build($args));
467 # Now we translate the ScriptBuilder objects back into self
468 $self->$_($$args{$_}) for @fields;
470 my @evts = @{$args->{_events}} if $args->{_events};
472 $logger->debug("circulator: script builder returned events: @evts") if @evts;
476 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
477 if(!$self->is_noncat and
479 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
483 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
484 return $self->bail_on_events(@e);
488 $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
490 # We can't renew if there is no copy
491 return $self->bail_on_events(@evts) if
492 $self->is_renewal and !$self->copy;
494 # Set some circ-specific flags in the script environment
495 my $evt = "environment";
496 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
498 if( $self->is_noncat ) {
499 $self->script_runner->insert("$evt.isNonCat", 1);
500 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
503 if( $self->is_precat ) {
504 $self->script_runner->insert("environment.isPrecat", 1, 1);
507 $self->script_runner->add_path( $_ ) for @$script_libs;
515 # --------------------------------------------------------------------------
516 # Does the circ permit work
517 # --------------------------------------------------------------------------
521 $self->log_me("do_permit()");
523 unless( $self->editor->requestor->id == $self->patron->id ) {
524 return $self->bail_on_events($self->editor->event)
525 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
528 $self->check_captured_holds();
529 $self->do_copy_checks();
530 return if $self->bail_out;
531 $self->run_patron_permit_scripts();
532 $self->run_copy_permit_scripts()
533 unless $self->is_precat or $self->is_noncat;
534 $self->override_events() unless $self->is_renewal;
535 return if $self->bail_out;
537 if( $self->is_precat ) {
540 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
541 return $self->bail_out(1) unless $self->is_renewal;
547 payload => $self->mk_permit_key));
551 sub check_captured_holds {
553 my $copy = $self->copy;
554 my $patron = $self->patron;
556 return undef unless $copy;
558 my $s = $U->copy_status($copy->status)->id;
559 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
560 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
562 # Item is on the holds shelf, make sure it's going to the right person
563 my $holds = $self->editor->search_action_hold_request(
566 current_copy => $copy->id ,
567 capture_time => { '!=' => undef },
568 cancel_time => undef,
569 fulfillment_time => undef
575 if( $holds and $$holds[0] ) {
576 return undef if $$holds[0]->usr == $patron->id;
579 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
581 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
587 my $copy = $self->copy;
590 my $stat = $U->copy_status($copy->status)->id;
592 # We cannot check out a copy if it is in-transit
593 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
594 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
597 $self->handle_claims_returned();
598 return if $self->bail_out;
600 # no claims returned circ was found, check if there is any open circ
601 unless( $self->is_renewal ) {
602 my $circs = $self->editor->search_action_circulation(
603 { target_copy => $copy->id, checkin_time => undef }
606 return $self->bail_on_events(
607 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
612 sub send_penalty_request {
614 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
615 $self->penalty_request(
617 'open-ils.penalty.patron_penalty.calculate',
619 authtoken => $self->editor->authtoken,
620 patron => $self->patron } ) );
623 sub gather_penalty_request {
625 return [] unless $self->penalty_request;
626 my $data = $self->penalty_request->recv;
628 $data = $data->content;
629 return $data->{fatal_penalties};
631 $logger->error("circulator: penalty request returned no data");
635 # ---------------------------------------------------------------------
636 # This pushes any patron-related events into the list but does not
637 # set bail_out for any events
638 # ---------------------------------------------------------------------
639 sub run_patron_permit_scripts {
641 my $runner = $self->script_runner;
642 my $patronid = $self->patron->id;
644 $self->send_penalty_request();
646 # ---------------------------------------------------------------------
647 # Now run the patron permit script
648 # ---------------------------------------------------------------------
649 $runner->load($self->circ_permit_patron);
650 my $result = $runner->run or
651 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
653 my $patron_events = $result->{events};
656 my $penalties = $self->gather_penalty_request();
658 for my $p (@$penalties, @$patron_events) {
660 # this is policy directly in the code, not a good idea in general, but
661 # the penalty server doesn't know anything about renewals, so we
662 # have to strip the event out here
663 next if $self->is_renewal and $p eq 'PATRON_EXCEEDS_OVERDUE_COUNT';
666 push( @allevents, OpenILS::Event->new($p))
669 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
671 $self->push_events(@allevents);
675 sub run_copy_permit_scripts {
677 my $copy = $self->copy || return;
678 my $runner = $self->script_runner;
680 # ---------------------------------------------------------------------
681 # Capture all of the copy permit events
682 # ---------------------------------------------------------------------
683 $runner->load($self->circ_permit_copy);
684 my $result = $runner->run or
685 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
686 my $copy_events = $result->{events};
688 # ---------------------------------------------------------------------
689 # Now collect all of the events together
690 # ---------------------------------------------------------------------
692 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
694 # See if this copy has an alert message
695 my $ae = $self->check_copy_alert();
696 push( @allevents, $ae ) if $ae;
698 # uniquify the events
699 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
700 @allevents = values %hash;
703 $_->{payload} = $copy if
704 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
707 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
709 $self->push_events(@allevents);
713 sub check_copy_alert {
715 return undef if $self->is_renewal;
716 return OpenILS::Event->new(
717 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
718 if $self->copy and $self->copy->alert_message;
724 # --------------------------------------------------------------------------
725 # If the call is overriding and has permissions to override every collected
726 # event, the are cleared. Any event that the caller does not have
727 # permission to override, will be left in the event list and bail_out will
729 # XXX We need code in here to cancel any holds/transits on copies
730 # that are being force-checked out
731 # --------------------------------------------------------------------------
732 sub override_events {
734 my @events = @{$self->events};
735 return unless @events;
737 if(!$self->override) {
738 return $self->bail_out(1)
739 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
744 for my $e (@events) {
745 my $tc = $e->{textcode};
746 next if $tc eq 'SUCCESS';
747 my $ov = "$tc.override";
748 $logger->info("circulator: attempting to override event: $ov");
750 return $self->bail_on_events($self->editor->event)
751 unless( $self->editor->allowed($ov) );
756 # --------------------------------------------------------------------------
757 # If there is an open claimsreturn circ on the requested copy, close the
758 # circ if overriding, otherwise bail out
759 # --------------------------------------------------------------------------
760 sub handle_claims_returned {
762 my $copy = $self->copy;
764 my $CR = $self->editor->search_action_circulation(
766 target_copy => $copy->id,
767 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
768 checkin_time => undef,
772 return unless ($CR = $CR->[0]);
776 # - If the caller has set the override flag, we will check the item in
777 if($self->override) {
779 $CR->checkin_time('now');
780 $CR->checkin_lib($self->editor->requestor->ws_ou);
781 $CR->checkin_staff($self->editor->requestor->id);
783 $evt = $self->editor->event
784 unless $self->editor->update_action_circulation($CR);
787 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
790 $self->bail_on_events($evt) if $evt;
795 # --------------------------------------------------------------------------
796 # This performs the checkout
797 # --------------------------------------------------------------------------
801 $self->log_me("do_checkout()");
803 # make sure perms are good if this isn't a renewal
804 unless( $self->is_renewal ) {
805 return $self->bail_on_events($self->editor->event)
806 unless( $self->editor->allowed('COPY_CHECKOUT') );
809 # verify the permit key
810 unless( $self->check_permit_key ) {
811 if( $self->permit_override ) {
812 return $self->bail_on_events($self->editor->event)
813 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
815 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
819 # if this is a non-cataloged circ, build the circ and finish
820 if( $self->is_noncat ) {
821 $self->checkout_noncat;
823 OpenILS::Event->new('SUCCESS',
824 payload => { noncat_circ => $self->circ }));
828 if( $self->is_precat ) {
829 $self->script_runner->insert("environment.isPrecat", 1, 1);
830 $self->make_precat_copy;
831 return if $self->bail_out;
833 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
834 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
837 $self->do_copy_checks;
838 return if $self->bail_out;
840 $self->run_checkout_scripts();
841 return if $self->bail_out;
843 $self->build_checkout_circ_object();
844 return if $self->bail_out;
846 $self->apply_modified_due_date();
847 return if $self->bail_out;
849 return $self->bail_on_events($self->editor->event)
850 unless $self->editor->create_action_circulation($self->circ);
852 # refresh the circ to force local time zone for now
853 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
855 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
857 return if $self->bail_out;
859 $self->handle_checkout_holds();
860 return if $self->bail_out;
863 # ------------------------------------------------------------------------------
864 # Update the patron penalty info in the DB
865 # ------------------------------------------------------------------------------
866 if( $self->permit_override ) {
867 $U->update_patron_penalties(
868 authtoken => $self->editor->authtoken,
869 patron => $self->patron,
874 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
876 OpenILS::Event->new('SUCCESS',
878 copy => $U->unflesh_copy($self->copy),
881 holds_fulfilled => $self->fulfilled_holds,
889 my $copy = $self->copy;
891 my $stat = $copy->status if ref $copy->status;
892 my $loc = $copy->location if ref $copy->location;
893 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
895 $copy->status($stat->id) if $stat;
896 $copy->location($loc->id) if $loc;
897 $copy->circ_lib($circ_lib->id) if $circ_lib;
898 $copy->editor($self->editor->requestor->id);
899 $copy->edit_date('now');
900 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
902 return $self->bail_on_events($self->editor->event)
903 unless $self->editor->update_asset_copy($self->copy);
905 $copy->status($U->copy_status($copy->status));
906 $copy->location($loc) if $loc;
907 $copy->circ_lib($circ_lib) if $circ_lib;
912 my( $self, @evts ) = @_;
913 $self->push_events(@evts);
917 sub handle_checkout_holds {
920 my $copy = $self->copy;
921 my $patron = $self->patron;
923 my $holds = $self->editor->search_action_hold_request(
925 current_copy => $copy->id ,
926 cancel_time => undef,
927 fulfillment_time => undef
933 # XXX We should only fulfill one hold here...
934 # XXX If a hold was transited to the user who is checking out
935 # the item, we need to make sure that hold is what's grabbed
938 # for now, just sort by id to get what should be the oldest hold
939 $holds = [ sort { $a->id <=> $b->id } @$holds ];
940 my @myholds = grep { $_->usr eq $patron->id } @$holds;
941 my @altholds = grep { $_->usr ne $patron->id } @$holds;
944 my $hold = $myholds[0];
946 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
948 # if the hold was never officially captured, capture it.
949 $hold->capture_time('now') unless $hold->capture_time;
951 # just make sure it's set correctly
952 $hold->current_copy($copy->id);
954 $hold->fulfillment_time('now');
955 $hold->fulfillment_staff($self->editor->requestor->id);
956 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
958 return $self->bail_on_events($self->editor->event)
959 unless $self->editor->update_action_hold_request($hold);
961 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
963 push( @fulfilled, $hold->id );
966 # If there are any holds placed for other users that point to this copy,
967 # then we need to un-target those holds so the targeter can pick a new copy
970 $logger->info("circulator: un-targeting hold ".$_->id.
971 " because copy ".$copy->id." is getting checked out");
973 # - make the targeter process this hold at next run
974 $_->clear_prev_check_time;
976 # - clear out the targetted copy
977 $_->clear_current_copy;
978 $_->clear_capture_time;
980 return $self->bail_on_event($self->editor->event)
981 unless $self->editor->update_action_hold_request($_);
985 $self->fulfilled_holds(\@fulfilled);
990 sub run_checkout_scripts {
994 my $runner = $self->script_runner;
995 $runner->load($self->circ_duration);
997 my $result = $runner->run or
998 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1000 my $duration = $result->{durationRule};
1001 my $recurring = $result->{recurringFinesRule};
1002 my $max_fine = $result->{maxFine};
1004 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1006 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1007 return $self->bail_on_events($evt) if $evt;
1009 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1010 return $self->bail_on_events($evt) if $evt;
1012 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1013 return $self->bail_on_events($evt) if $evt;
1017 # The item circulates with an unlimited duration
1023 $self->duration_rule($duration);
1024 $self->recurring_fines_rule($recurring);
1025 $self->max_fine_rule($max_fine);
1029 sub build_checkout_circ_object {
1032 my $circ = Fieldmapper::action::circulation->new;
1033 my $duration = $self->duration_rule;
1034 my $max = $self->max_fine_rule;
1035 my $recurring = $self->recurring_fines_rule;
1036 my $copy = $self->copy;
1037 my $patron = $self->patron;
1041 my $dname = $duration->name;
1042 my $mname = $max->name;
1043 my $rname = $recurring->name;
1045 $logger->debug("circulator: building circulation ".
1046 "with duration=$dname, maxfine=$mname, recurring=$rname");
1048 $circ->duration( $duration->shrt )
1049 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1050 $circ->duration( $duration->normal )
1051 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1052 $circ->duration( $duration->extended )
1053 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1055 $circ->recuring_fine( $recurring->low )
1056 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1057 $circ->recuring_fine( $recurring->normal )
1058 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1059 $circ->recuring_fine( $recurring->high )
1060 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1062 $circ->duration_rule( $duration->name );
1063 $circ->recuring_fine_rule( $recurring->name );
1064 $circ->max_fine_rule( $max->name );
1065 $circ->max_fine( $max->amount );
1067 $circ->fine_interval($recurring->recurance_interval);
1068 $circ->renewal_remaining( $duration->max_renewals );
1072 $logger->info("circulator: copy found with an unlimited circ duration");
1073 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1074 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1075 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1076 $circ->renewal_remaining(0);
1079 $circ->target_copy( $copy->id );
1080 $circ->usr( $patron->id );
1081 $circ->circ_lib( $self->circ_lib );
1083 if( $self->is_renewal ) {
1084 $circ->opac_renewal(1);
1085 $circ->renewal_remaining($self->renewal_remaining);
1086 $circ->circ_staff($self->editor->requestor->id);
1089 # if the user provided an overiding checkout time,
1090 # (e.g. the checkout really happened several hours ago), then
1091 # we apply that here. Does this need a perm??
1092 $circ->xact_start(clense_ISO8601($self->checkout_time))
1093 if $self->checkout_time;
1095 # if a patron is renewing, 'requestor' will be the patron
1096 $circ->circ_staff($self->editor->requestor->id);
1097 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1103 sub apply_modified_due_date {
1105 my $circ = $self->circ;
1106 my $copy = $self->copy;
1108 if( $self->due_date ) {
1110 return $self->bail_on_events($self->editor->event)
1111 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1113 $circ->due_date(clense_ISO8601($self->due_date));
1117 # if the due_date lands on a day when the location is closed
1118 return unless $copy and $circ->due_date;
1120 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1122 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1123 " with an item due date of ".$circ->due_date );
1125 my $dateinfo = $U->storagereq(
1126 'open-ils.storage.actor.org_unit.closed_date.overlap',
1127 $org, $circ->due_date );
1130 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1131 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1133 # XXX make the behavior more dynamic
1134 # for now, we just push the due date to after the close date
1135 $circ->due_date($dateinfo->{end});
1142 sub create_due_date {
1143 my( $self, $duration ) = @_;
1144 my ($sec,$min,$hour,$mday,$mon,$year) =
1145 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1146 $year += 1900; $mon += 1;
1147 my $due_date = sprintf(
1148 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1149 $year, $mon, $mday, $hour, $min, $sec);
1155 sub make_precat_copy {
1157 my $copy = $self->copy;
1160 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1162 $copy->editor($self->editor->requestor->id);
1163 $copy->edit_date('now');
1164 $copy->dummy_title($self->dummy_title);
1165 $copy->dummy_author($self->dummy_author);
1167 $self->update_copy();
1171 $logger->info("circulator: Creating a new precataloged ".
1172 "copy in checkout with barcode " . $self->copy_barcode);
1174 $copy = Fieldmapper::asset::copy->new;
1175 $copy->circ_lib($self->circ_lib);
1176 $copy->creator($self->editor->requestor->id);
1177 $copy->editor($self->editor->requestor->id);
1178 $copy->barcode($self->copy_barcode);
1179 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1180 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1181 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1183 $copy->dummy_title($self->dummy_title || "");
1184 $copy->dummy_author($self->dummy_author || "");
1186 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1188 $self->push_events($self->editor->event);
1192 # this is a little bit of a hack, but we need to
1193 # get the copy into the script runner
1194 $self->script_runner->insert("environment.copy", $copy, 1);
1198 sub checkout_noncat {
1204 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1205 my $count = $self->noncat_count || 1;
1206 my $cotime = clense_ISO8601($self->checkout_time) || "";
1208 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1212 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1213 $self->editor->requestor->id,
1221 $self->push_events($evt);
1232 $self->log_me("do_checkin()");
1235 return $self->bail_on_events(
1236 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1239 if( $self->checkin_check_holds_shelf() ) {
1240 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1241 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1242 $self->checkin_flesh_events;
1246 unless( $self->is_renewal ) {
1247 return $self->bail_on_events($self->editor->event)
1248 unless $self->editor->allowed('COPY_CHECKIN');
1251 $self->push_events($self->check_copy_alert());
1252 $self->push_events($self->check_checkin_copy_status());
1254 # the renew code will have already found our circulation object
1255 unless( $self->is_renewal and $self->circ ) {
1257 $self->editor->search_action_circulation(
1258 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1261 # if the circ is marked as 'claims returned', add the event to the list
1262 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1263 if ($self->circ and $self->circ->stop_fines
1264 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1266 # handle the overridable events
1267 $self->override_events unless $self->is_renewal;
1268 return if $self->bail_out;
1272 $self->editor->search_action_transit_copy(
1273 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1277 $self->checkin_handle_circ;
1278 return if $self->bail_out;
1279 $self->checkin_changed(1);
1281 } elsif( $self->transit ) {
1282 my $hold_transit = $self->process_received_transit;
1283 $self->checkin_changed(1);
1285 if( $self->bail_out ) {
1286 $self->checkin_flesh_events;
1290 if( my $e = $self->check_checkin_copy_status() ) {
1291 # If the original copy status is special, alert the caller
1292 my $ev = $self->events;
1293 $self->events([$e]);
1294 $self->override_events;
1295 return if $self->bail_out;
1299 if( $hold_transit or
1300 $U->copy_status($self->copy->status)->id
1301 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1304 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1305 $U->fetch_open_hold_by_copy($self->copy->id)
1308 $self->checkin_flesh_events;
1312 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1313 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1314 " that is in-transit, but there is no transit.. repairing");
1315 $self->reshelve_copy(1);
1316 return if $self->bail_out;
1319 if( $self->is_renewal ) {
1320 $self->push_events(OpenILS::Event->new('SUCCESS'));
1324 # ------------------------------------------------------------------------------
1325 # Circulations and transits are now closed where necessary. Now go on to see if
1326 # this copy can fulfill a hold or needs to be routed to a different location
1327 # ------------------------------------------------------------------------------
1329 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1330 return if $self->bail_out;
1332 } else { # not needed for a hold
1335 my $circ_lib = (ref $self->copy->circ_lib) ?
1336 $self->copy->circ_lib->id : $self->copy->circ_lib;
1338 if( $self->remote_hold ) {
1339 $circ_lib = $self->remote_hold->pickup_lib;
1340 $logger->warn("circulator: Copy ".$self->copy->barcode.
1341 " is on a remote hold's shelf, sending to $circ_lib");
1344 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1346 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1348 $self->checkin_handle_precat();
1349 return if $self->bail_out;
1353 my $bc = $self->copy->barcode;
1354 $logger->info("circulator: copy $bc at a remote lib - sending home");
1355 $self->checkin_build_copy_transit();
1356 return if $self->bail_out;
1357 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1361 $self->reshelve_copy;
1362 return if $self->bail_out;
1364 unless($self->checkin_changed) {
1366 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1367 my $stat = $U->copy_status($self->copy->status)->id;
1369 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1370 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1371 $self->bail_out(1); # no need to commit anything
1374 $self->push_events(OpenILS::Event->new('SUCCESS'))
1375 unless @{$self->events};
1379 # ------------------------------------------------------------------------------
1380 # Update the patron penalty info in the DB
1381 # ------------------------------------------------------------------------------
1382 $U->update_patron_penalties(
1383 authtoken => $self->editor->authtoken,
1384 patron => $self->patron,
1385 background => 1 ) if $self->is_checkin;
1387 $self->checkin_flesh_events;
1393 my $force = $self->force || shift;
1394 my $copy = $self->copy;
1396 my $stat = $U->copy_status($copy->status)->id;
1399 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1400 $stat != OILS_COPY_STATUS_CATALOGING and
1401 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1402 $stat != OILS_COPY_STATUS_RESHELVING )) {
1404 $copy->status( OILS_COPY_STATUS_RESHELVING );
1406 $self->checkin_changed(1);
1411 # Returns true if the item is at the current location
1412 # because it was transited there for a hold and the
1413 # hold has not been fulfilled
1414 sub checkin_check_holds_shelf {
1416 return 0 unless $self->copy;
1419 $U->copy_status($self->copy->status)->id ==
1420 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1422 # find the hold that put us on the holds shelf
1423 my $holds = $self->editor->search_action_hold_request(
1425 current_copy => $self->copy->id,
1426 capture_time => { '!=' => undef },
1427 fulfillment_time => undef,
1428 cancel_time => undef,
1433 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1434 $self->reshelve_copy(1);
1438 my $hold = $$holds[0];
1440 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1441 $hold->id. "] for copy ".$self->copy->barcode);
1443 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1444 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1448 $logger->info("circulator: hold is not for here..");
1449 $self->remote_hold($hold);
1454 sub checkin_handle_precat {
1456 my $copy = $self->copy;
1458 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1459 $copy->status(OILS_COPY_STATUS_CATALOGING);
1460 $self->update_copy();
1461 $self->checkin_changed(1);
1462 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1467 sub checkin_build_copy_transit {
1469 my $copy = $self->copy;
1470 my $transit = Fieldmapper::action::transit_copy->new;
1472 $transit->source($self->editor->requestor->ws_ou);
1473 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1474 $transit->target_copy($copy->id);
1475 $transit->source_send_time('now');
1476 $transit->copy_status( $U->copy_status($copy->status)->id );
1478 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1480 return $self->bail_on_events($self->editor->event)
1481 unless $self->editor->create_action_transit_copy($transit);
1483 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1485 $self->checkin_changed(1);
1489 sub attempt_checkin_hold_capture {
1491 my $copy = $self->copy;
1493 # See if this copy can fulfill any holds
1494 my ($hold) = $holdcode->find_nearest_permitted_hold(
1495 OpenSRF::AppSession->create('open-ils.storage'),
1496 $copy, $self->editor->requestor );
1499 $logger->debug("circulator: no potential permitted".
1500 "holds found for copy ".$copy->barcode);
1505 $logger->info("circulator: found permitted hold ".
1506 $hold->id . " for copy, capturing...");
1508 $hold->current_copy($copy->id);
1509 $hold->capture_time('now');
1511 # prevent DB errors caused by fetching
1512 # holds from storage, and updating through cstore
1513 $hold->clear_fulfillment_time;
1514 $hold->clear_fulfillment_staff;
1515 $hold->clear_fulfillment_lib;
1516 $hold->clear_expire_time;
1517 $hold->clear_cancel_time;
1518 $hold->clear_prev_check_time unless $hold->prev_check_time;
1520 $self->bail_on_events($self->editor->event)
1521 unless $self->editor->update_action_hold_request($hold);
1523 $self->checkin_changed(1);
1525 return 1 if $self->bail_out;
1527 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1529 # This hold was captured in the correct location
1530 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1531 $self->push_events(OpenILS::Event->new('SUCCESS'));
1533 #$self->do_hold_notify($hold->id);
1534 $self->notify_hold($hold->id);
1538 # Hold needs to be picked up elsewhere. Build a hold
1539 # transit and route the item.
1540 $self->checkin_build_hold_transit();
1541 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1542 return 1 if $self->bail_out;
1544 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1547 # make sure we save the copy status
1552 sub do_hold_notify {
1553 my( $self, $holdid ) = @_;
1555 $logger->info("circulator: running delayed hold notify process");
1557 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1558 hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1560 $logger->debug("circulator: built hold notifier");
1562 if(!$notifier->event) {
1564 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1566 my $stat = $notifier->send_email_notify;
1567 if( $stat == '1' ) {
1568 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1572 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1575 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1580 sub checkin_build_hold_transit {
1583 my $copy = $self->copy;
1584 my $hold = $self->hold;
1585 my $trans = Fieldmapper::action::hold_transit_copy->new;
1587 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1589 $trans->hold($hold->id);
1590 $trans->source($self->editor->requestor->ws_ou);
1591 $trans->dest($hold->pickup_lib);
1592 $trans->source_send_time("now");
1593 $trans->target_copy($copy->id);
1595 # when the copy gets to its destination, it will recover
1596 # this status - put it onto the holds shelf
1597 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1599 return $self->bail_on_events($self->editor->event)
1600 unless $self->editor->create_action_hold_transit_copy($trans);
1605 sub process_received_transit {
1607 my $copy = $self->copy;
1608 my $copyid = $self->copy->id;
1610 my $status_name = $U->copy_status($copy->status)->name;
1611 $logger->debug("circulator: attempting transit receive on ".
1612 "copy $copyid. Copy status is $status_name");
1614 my $transit = $self->transit;
1616 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1617 $logger->info("circulator: Fowarding transit on copy which is destined ".
1618 "for a different location. copy=$copyid,current ".
1619 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1621 return $self->bail_on_events(
1622 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1625 # The transit is received, set the receive time
1626 $transit->dest_recv_time('now');
1627 $self->bail_on_events($self->editor->event)
1628 unless $self->editor->update_action_transit_copy($transit);
1630 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1632 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1633 $copy->status( $transit->copy_status );
1634 $self->update_copy();
1635 return if $self->bail_out;
1639 #$self->do_hold_notify($hold_transit->hold);
1640 $self->notify_hold($hold_transit->hold);
1645 OpenILS::Event->new(
1648 payload => { transit => $transit, holdtransit => $hold_transit } ));
1650 return $hold_transit;
1654 sub checkin_handle_circ {
1658 my $circ = $self->circ;
1659 my $copy = $self->copy;
1663 # backdate the circ if necessary
1664 if($self->backdate) {
1665 $self->checkin_handle_backdate;
1666 return if $self->bail_out;
1669 if(!$circ->stop_fines) {
1670 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1671 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1672 $circ->stop_fines_time('now') unless $self->backdate;
1673 $circ->stop_fines_time($self->backdate) if $self->backdate;
1676 # see if there are any fines owed on this circ. if not, close it
1677 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1678 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1680 # Set the checkin vars since we have the item
1681 $circ->checkin_time('now');
1682 $circ->checkin_staff($self->editor->requestor->id);
1683 $circ->checkin_lib($self->editor->requestor->ws_ou);
1685 my $circ_lib = (ref $self->copy->circ_lib) ?
1686 $self->copy->circ_lib->id : $self->copy->circ_lib;
1687 my $stat = $U->copy_status($self->copy->status)->id;
1689 # If the item is lost/missing and it needs to be sent home, don't
1690 # reshelve the copy, leave it lost/missing so the recipient will know
1691 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1692 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1693 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1696 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1701 return $self->bail_on_events($self->editor->event)
1702 unless $self->editor->update_action_circulation($circ);
1706 sub checkin_handle_backdate {
1709 my $bd = $self->backdate;
1710 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1711 $bd = "${bd}T23:59:59";
1713 my $bills = $self->editor->search_money_billing(
1715 billing_ts => { '>=' => $bd },
1716 xact => $self->circ->id,
1717 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1721 for my $bill (@$bills) {
1722 if( !$bill->voided or $bill->voided =~ /f/i ) {
1724 my $n = $bill->note || "";
1725 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1727 $self->bail_on_events($self->editor->event)
1728 unless $self->editor->update_money_billing($bill);
1735 # XXX Legacy version for Circ.pm support
1736 sub _checkin_handle_backdate {
1737 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1740 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1741 $bd = "${bd}T23:59:59";
1744 my $bills = $session->request(
1745 "open-ils.storage.direct.money.billing.search_where.atomic",
1746 billing_ts => { '>=' => $bd },
1748 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1752 for my $bill (@$bills) {
1754 my $n = $bill->note || "";
1755 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1756 my $s = $session->request(
1757 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1758 return $U->DB_UPDATE_FAILED($bill) unless $s;
1768 sub find_patron_from_copy {
1770 my $circs = $self->editor->search_action_circulation(
1771 { target_copy => $self->copy->id, checkin_time => undef });
1772 my $circ = $circs->[0];
1773 return unless $circ;
1774 my $u = $self->editor->retrieve_actor_user($circ->usr)
1775 or return $self->bail_on_events($self->editor->event);
1779 sub check_checkin_copy_status {
1781 my $copy = $self->copy;
1787 my $status = $U->copy_status($copy->status)->id;
1790 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1791 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1792 $status == OILS_COPY_STATUS_IN_PROCESS ||
1793 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1794 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1795 $status == OILS_COPY_STATUS_CATALOGING ||
1796 $status == OILS_COPY_STATUS_RESHELVING );
1798 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1799 if( $status == OILS_COPY_STATUS_LOST );
1801 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1802 if( $status == OILS_COPY_STATUS_MISSING );
1804 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1809 # --------------------------------------------------------------------------
1810 # On checkin, we need to return as many relevant objects as we can
1811 # --------------------------------------------------------------------------
1812 sub checkin_flesh_events {
1815 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1816 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1817 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1821 for my $evt (@{$self->events}) {
1824 $payload->{copy} = $U->unflesh_copy($self->copy);
1825 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1826 $payload->{circ} = $self->circ;
1827 $payload->{transit} = $self->transit;
1828 $payload->{hold} = $self->hold;
1830 $evt->{payload} = $payload;
1835 my( $self, $msg ) = @_;
1836 my $bc = ($self->copy) ? $self->copy->barcode :
1839 my $usr = ($self->patron) ? $self->patron->id : "";
1840 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1841 ", recipient=$usr, copy=$bc");
1847 $self->log_me("do_renew()");
1848 $self->is_renewal(1);
1850 unless( $self->is_renewal ) {
1851 return $self->bail_on_events($self->editor->events)
1852 unless $self->editor->allowed('RENEW_CIRC');
1855 # Make sure there is an open circ to renew that is not
1856 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1857 my $circ = $self->editor->search_action_circulation(
1858 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1861 $circ = $self->editor->search_action_circulation(
1863 target_copy => $self->copy->id,
1864 stop_fines => OILS_STOP_FINES_MAX_FINES,
1865 checkin_time => undef
1870 return $self->bail_on_events($self->editor->event) unless $circ;
1872 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1873 if $circ->renewal_remaining < 1;
1875 # -----------------------------------------------------------------
1877 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1880 $self->run_renew_permit;
1883 $self->do_checkin();
1884 return if $self->bail_out;
1886 unless( $self->permit_override ) {
1888 return if $self->bail_out;
1889 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1890 $self->remove_event('ITEM_NOT_CATALOGED');
1893 $self->override_events;
1894 return if $self->bail_out;
1897 $self->do_checkout();
1902 my( $self, $evt ) = @_;
1903 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1904 $logger->debug("circulator: removing event from list: $evt");
1905 my @events = @{$self->events};
1906 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1911 my( $self, $evt ) = @_;
1912 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1913 return grep { $_->{textcode} eq $evt } @{$self->events};
1918 sub run_renew_permit {
1920 my $runner = $self->script_runner;
1922 $runner->load($self->circ_permit_renew);
1923 my $result = $runner->run or
1924 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1925 my $events = $result->{events};
1927 $logger->activity("ciculator: circ_permit_renew for user ".
1928 $self->patron->id." returned events: @$events") if @$events;
1930 $self->push_events(OpenILS::Event->new($_)) for @$events;
1932 $logger->debug("circulator: re-creating script runner to be safe");
1933 $self->mk_script_runner;