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() unless $self->is_renewal;
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};
657 # ---------------------------------------------------------------------
658 # this is policy directly in the code, not a good idea in general, but
659 # the penalty server doesn't know anything about renewals, so we
660 # have to strip the event out here
661 my $penalties = ($self->is_renewal) ? [] : $self->gather_penalty_request();
662 # ---------------------------------------------------------------------
664 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
666 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
668 $self->push_events(@allevents);
672 sub run_copy_permit_scripts {
674 my $copy = $self->copy || return;
675 my $runner = $self->script_runner;
677 # ---------------------------------------------------------------------
678 # Capture all of the copy permit events
679 # ---------------------------------------------------------------------
680 $runner->load($self->circ_permit_copy);
681 my $result = $runner->run or
682 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
683 my $copy_events = $result->{events};
685 # ---------------------------------------------------------------------
686 # Now collect all of the events together
687 # ---------------------------------------------------------------------
689 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
691 # See if this copy has an alert message
692 my $ae = $self->check_copy_alert();
693 push( @allevents, $ae ) if $ae;
695 # uniquify the events
696 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
697 @allevents = values %hash;
700 $_->{payload} = $copy if
701 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
704 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
706 $self->push_events(@allevents);
710 sub check_copy_alert {
712 return undef if $self->is_renewal;
713 return OpenILS::Event->new(
714 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
715 if $self->copy and $self->copy->alert_message;
721 # --------------------------------------------------------------------------
722 # If the call is overriding and has permissions to override every collected
723 # event, the are cleared. Any event that the caller does not have
724 # permission to override, will be left in the event list and bail_out will
726 # XXX We need code in here to cancel any holds/transits on copies
727 # that are being force-checked out
728 # --------------------------------------------------------------------------
729 sub override_events {
731 my @events = @{$self->events};
732 return unless @events;
734 if(!$self->override) {
735 return $self->bail_out(1)
736 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
741 for my $e (@events) {
742 my $tc = $e->{textcode};
743 next if $tc eq 'SUCCESS';
744 my $ov = "$tc.override";
745 $logger->info("circulator: attempting to override event: $ov");
747 return $self->bail_on_events($self->editor->event)
748 unless( $self->editor->allowed($ov) );
753 # --------------------------------------------------------------------------
754 # If there is an open claimsreturn circ on the requested copy, close the
755 # circ if overriding, otherwise bail out
756 # --------------------------------------------------------------------------
757 sub handle_claims_returned {
759 my $copy = $self->copy;
761 my $CR = $self->editor->search_action_circulation(
763 target_copy => $copy->id,
764 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
765 checkin_time => undef,
769 return unless ($CR = $CR->[0]);
773 # - If the caller has set the override flag, we will check the item in
774 if($self->override) {
776 $CR->checkin_time('now');
777 $CR->checkin_lib($self->editor->requestor->ws_ou);
778 $CR->checkin_staff($self->editor->requestor->id);
780 $evt = $self->editor->event
781 unless $self->editor->update_action_circulation($CR);
784 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
787 $self->bail_on_events($evt) if $evt;
792 # --------------------------------------------------------------------------
793 # This performs the checkout
794 # --------------------------------------------------------------------------
798 $self->log_me("do_checkout()");
800 # make sure perms are good if this isn't a renewal
801 unless( $self->is_renewal ) {
802 return $self->bail_on_events($self->editor->event)
803 unless( $self->editor->allowed('COPY_CHECKOUT') );
806 # verify the permit key
807 unless( $self->check_permit_key ) {
808 if( $self->permit_override ) {
809 return $self->bail_on_events($self->editor->event)
810 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
812 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
816 # if this is a non-cataloged circ, build the circ and finish
817 if( $self->is_noncat ) {
818 $self->checkout_noncat;
820 OpenILS::Event->new('SUCCESS',
821 payload => { noncat_circ => $self->circ }));
825 if( $self->is_precat ) {
826 $self->script_runner->insert("environment.isPrecat", 1, 1);
827 $self->make_precat_copy;
828 return if $self->bail_out;
830 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
831 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
834 $self->do_copy_checks;
835 return if $self->bail_out;
837 $self->run_checkout_scripts();
838 return if $self->bail_out;
840 $self->build_checkout_circ_object();
841 return if $self->bail_out;
843 $self->apply_modified_due_date();
844 return if $self->bail_out;
846 return $self->bail_on_events($self->editor->event)
847 unless $self->editor->create_action_circulation($self->circ);
849 # refresh the circ to force local time zone for now
850 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
852 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
854 return if $self->bail_out;
856 $self->handle_checkout_holds();
857 return if $self->bail_out;
859 # ------------------------------------------------------------------------------
860 # Update the patron penalty info in the DB. Run it for permit-overrides or
861 # renewals since both of those cases do not require the penalty server to
862 # run during the permit phase of the checkout
863 # ------------------------------------------------------------------------------
864 if( $self->permit_override or $self->is_renewal ) {
865 $U->update_patron_penalties(
866 authtoken => $self->editor->authtoken,
867 patron => $self->patron,
872 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
874 OpenILS::Event->new('SUCCESS',
876 copy => $U->unflesh_copy($self->copy),
879 holds_fulfilled => $self->fulfilled_holds,
887 my $copy = $self->copy;
889 my $stat = $copy->status if ref $copy->status;
890 my $loc = $copy->location if ref $copy->location;
891 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
893 $copy->status($stat->id) if $stat;
894 $copy->location($loc->id) if $loc;
895 $copy->circ_lib($circ_lib->id) if $circ_lib;
896 $copy->editor($self->editor->requestor->id);
897 $copy->edit_date('now');
898 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
900 return $self->bail_on_events($self->editor->event)
901 unless $self->editor->update_asset_copy($self->copy);
903 $copy->status($U->copy_status($copy->status));
904 $copy->location($loc) if $loc;
905 $copy->circ_lib($circ_lib) if $circ_lib;
910 my( $self, @evts ) = @_;
911 $self->push_events(@evts);
915 sub handle_checkout_holds {
918 my $copy = $self->copy;
919 my $patron = $self->patron;
921 my $holds = $self->editor->search_action_hold_request(
923 current_copy => $copy->id ,
924 cancel_time => undef,
925 fulfillment_time => undef
931 # XXX We should only fulfill one hold here...
932 # XXX If a hold was transited to the user who is checking out
933 # the item, we need to make sure that hold is what's grabbed
936 # for now, just sort by id to get what should be the oldest hold
937 $holds = [ sort { $a->id <=> $b->id } @$holds ];
938 my @myholds = grep { $_->usr eq $patron->id } @$holds;
939 my @altholds = grep { $_->usr ne $patron->id } @$holds;
942 my $hold = $myholds[0];
944 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
946 # if the hold was never officially captured, capture it.
947 $hold->capture_time('now') unless $hold->capture_time;
949 # just make sure it's set correctly
950 $hold->current_copy($copy->id);
952 $hold->fulfillment_time('now');
953 $hold->fulfillment_staff($self->editor->requestor->id);
954 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
956 return $self->bail_on_events($self->editor->event)
957 unless $self->editor->update_action_hold_request($hold);
959 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
961 push( @fulfilled, $hold->id );
964 # If there are any holds placed for other users that point to this copy,
965 # then we need to un-target those holds so the targeter can pick a new copy
968 $logger->info("circulator: un-targeting hold ".$_->id.
969 " because copy ".$copy->id." is getting checked out");
971 # - make the targeter process this hold at next run
972 $_->clear_prev_check_time;
974 # - clear out the targetted copy
975 $_->clear_current_copy;
976 $_->clear_capture_time;
978 return $self->bail_on_event($self->editor->event)
979 unless $self->editor->update_action_hold_request($_);
983 $self->fulfilled_holds(\@fulfilled);
988 sub run_checkout_scripts {
992 my $runner = $self->script_runner;
993 $runner->load($self->circ_duration);
995 my $result = $runner->run or
996 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
998 my $duration = $result->{durationRule};
999 my $recurring = $result->{recurringFinesRule};
1000 my $max_fine = $result->{maxFine};
1002 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1004 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1005 return $self->bail_on_events($evt) if $evt;
1007 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1008 return $self->bail_on_events($evt) if $evt;
1010 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1011 return $self->bail_on_events($evt) if $evt;
1015 # The item circulates with an unlimited duration
1021 $self->duration_rule($duration);
1022 $self->recurring_fines_rule($recurring);
1023 $self->max_fine_rule($max_fine);
1027 sub build_checkout_circ_object {
1030 my $circ = Fieldmapper::action::circulation->new;
1031 my $duration = $self->duration_rule;
1032 my $max = $self->max_fine_rule;
1033 my $recurring = $self->recurring_fines_rule;
1034 my $copy = $self->copy;
1035 my $patron = $self->patron;
1039 my $dname = $duration->name;
1040 my $mname = $max->name;
1041 my $rname = $recurring->name;
1043 $logger->debug("circulator: building circulation ".
1044 "with duration=$dname, maxfine=$mname, recurring=$rname");
1046 $circ->duration( $duration->shrt )
1047 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1048 $circ->duration( $duration->normal )
1049 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1050 $circ->duration( $duration->extended )
1051 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1053 $circ->recuring_fine( $recurring->low )
1054 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1055 $circ->recuring_fine( $recurring->normal )
1056 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1057 $circ->recuring_fine( $recurring->high )
1058 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1060 $circ->duration_rule( $duration->name );
1061 $circ->recuring_fine_rule( $recurring->name );
1062 $circ->max_fine_rule( $max->name );
1063 $circ->max_fine( $max->amount );
1065 $circ->fine_interval($recurring->recurance_interval);
1066 $circ->renewal_remaining( $duration->max_renewals );
1070 $logger->info("circulator: copy found with an unlimited circ duration");
1071 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1072 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1073 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1074 $circ->renewal_remaining(0);
1077 $circ->target_copy( $copy->id );
1078 $circ->usr( $patron->id );
1079 $circ->circ_lib( $self->circ_lib );
1081 if( $self->is_renewal ) {
1082 $circ->opac_renewal(1);
1083 $circ->renewal_remaining($self->renewal_remaining);
1084 $circ->circ_staff($self->editor->requestor->id);
1087 # if the user provided an overiding checkout time,
1088 # (e.g. the checkout really happened several hours ago), then
1089 # we apply that here. Does this need a perm??
1090 $circ->xact_start(clense_ISO8601($self->checkout_time))
1091 if $self->checkout_time;
1093 # if a patron is renewing, 'requestor' will be the patron
1094 $circ->circ_staff($self->editor->requestor->id);
1095 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1101 sub apply_modified_due_date {
1103 my $circ = $self->circ;
1104 my $copy = $self->copy;
1106 if( $self->due_date ) {
1108 return $self->bail_on_events($self->editor->event)
1109 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1111 $circ->due_date(clense_ISO8601($self->due_date));
1115 # if the due_date lands on a day when the location is closed
1116 return unless $copy and $circ->due_date;
1118 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1120 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1121 " with an item due date of ".$circ->due_date );
1123 my $dateinfo = $U->storagereq(
1124 'open-ils.storage.actor.org_unit.closed_date.overlap',
1125 $org, $circ->due_date );
1128 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1129 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1131 # XXX make the behavior more dynamic
1132 # for now, we just push the due date to after the close date
1133 $circ->due_date($dateinfo->{end});
1140 sub create_due_date {
1141 my( $self, $duration ) = @_;
1142 my ($sec,$min,$hour,$mday,$mon,$year) =
1143 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1144 $year += 1900; $mon += 1;
1145 my $due_date = sprintf(
1146 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1147 $year, $mon, $mday, $hour, $min, $sec);
1153 sub make_precat_copy {
1155 my $copy = $self->copy;
1158 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1160 $copy->editor($self->editor->requestor->id);
1161 $copy->edit_date('now');
1162 $copy->dummy_title($self->dummy_title);
1163 $copy->dummy_author($self->dummy_author);
1165 $self->update_copy();
1169 $logger->info("circulator: Creating a new precataloged ".
1170 "copy in checkout with barcode " . $self->copy_barcode);
1172 $copy = Fieldmapper::asset::copy->new;
1173 $copy->circ_lib($self->circ_lib);
1174 $copy->creator($self->editor->requestor->id);
1175 $copy->editor($self->editor->requestor->id);
1176 $copy->barcode($self->copy_barcode);
1177 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1178 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1179 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1181 $copy->dummy_title($self->dummy_title || "");
1182 $copy->dummy_author($self->dummy_author || "");
1184 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1186 $self->push_events($self->editor->event);
1190 # this is a little bit of a hack, but we need to
1191 # get the copy into the script runner
1192 $self->script_runner->insert("environment.copy", $copy, 1);
1196 sub checkout_noncat {
1202 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1203 my $count = $self->noncat_count || 1;
1204 my $cotime = clense_ISO8601($self->checkout_time) || "";
1206 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1210 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1211 $self->editor->requestor->id,
1219 $self->push_events($evt);
1230 $self->log_me("do_checkin()");
1233 return $self->bail_on_events(
1234 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1237 if( $self->checkin_check_holds_shelf() ) {
1238 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1239 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1240 $self->checkin_flesh_events;
1244 unless( $self->is_renewal ) {
1245 return $self->bail_on_events($self->editor->event)
1246 unless $self->editor->allowed('COPY_CHECKIN');
1249 $self->push_events($self->check_copy_alert());
1250 $self->push_events($self->check_checkin_copy_status());
1252 # the renew code will have already found our circulation object
1253 unless( $self->is_renewal and $self->circ ) {
1255 $self->editor->search_action_circulation(
1256 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1259 # if the circ is marked as 'claims returned', add the event to the list
1260 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1261 if ($self->circ and $self->circ->stop_fines
1262 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1264 # handle the overridable events
1265 $self->override_events unless $self->is_renewal;
1266 return if $self->bail_out;
1270 $self->editor->search_action_transit_copy(
1271 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1275 $self->checkin_handle_circ;
1276 return if $self->bail_out;
1277 $self->checkin_changed(1);
1279 } elsif( $self->transit ) {
1280 my $hold_transit = $self->process_received_transit;
1281 $self->checkin_changed(1);
1283 if( $self->bail_out ) {
1284 $self->checkin_flesh_events;
1288 if( my $e = $self->check_checkin_copy_status() ) {
1289 # If the original copy status is special, alert the caller
1290 my $ev = $self->events;
1291 $self->events([$e]);
1292 $self->override_events;
1293 return if $self->bail_out;
1297 if( $hold_transit or
1298 $U->copy_status($self->copy->status)->id
1299 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1302 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1303 $U->fetch_open_hold_by_copy($self->copy->id)
1306 $self->checkin_flesh_events;
1310 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1311 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1312 " that is in-transit, but there is no transit.. repairing");
1313 $self->reshelve_copy(1);
1314 return if $self->bail_out;
1317 if( $self->is_renewal ) {
1318 $self->push_events(OpenILS::Event->new('SUCCESS'));
1322 # ------------------------------------------------------------------------------
1323 # Circulations and transits are now closed where necessary. Now go on to see if
1324 # this copy can fulfill a hold or needs to be routed to a different location
1325 # ------------------------------------------------------------------------------
1327 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1328 return if $self->bail_out;
1330 } else { # not needed for a hold
1333 my $circ_lib = (ref $self->copy->circ_lib) ?
1334 $self->copy->circ_lib->id : $self->copy->circ_lib;
1336 if( $self->remote_hold ) {
1337 $circ_lib = $self->remote_hold->pickup_lib;
1338 $logger->warn("circulator: Copy ".$self->copy->barcode.
1339 " is on a remote hold's shelf, sending to $circ_lib");
1342 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1344 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1346 $self->checkin_handle_precat();
1347 return if $self->bail_out;
1351 my $bc = $self->copy->barcode;
1352 $logger->info("circulator: copy $bc at a remote lib - sending home");
1353 $self->checkin_build_copy_transit();
1354 return if $self->bail_out;
1355 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1359 $self->reshelve_copy;
1360 return if $self->bail_out;
1362 unless($self->checkin_changed) {
1364 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1365 my $stat = $U->copy_status($self->copy->status)->id;
1367 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1368 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1369 $self->bail_out(1); # no need to commit anything
1372 $self->push_events(OpenILS::Event->new('SUCCESS'))
1373 unless @{$self->events};
1377 # ------------------------------------------------------------------------------
1378 # Update the patron penalty info in the DB
1379 # ------------------------------------------------------------------------------
1380 $U->update_patron_penalties(
1381 authtoken => $self->editor->authtoken,
1382 patron => $self->patron,
1383 background => 1 ) if $self->is_checkin;
1385 $self->checkin_flesh_events;
1391 my $force = $self->force || shift;
1392 my $copy = $self->copy;
1394 my $stat = $U->copy_status($copy->status)->id;
1397 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1398 $stat != OILS_COPY_STATUS_CATALOGING and
1399 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1400 $stat != OILS_COPY_STATUS_RESHELVING )) {
1402 $copy->status( OILS_COPY_STATUS_RESHELVING );
1404 $self->checkin_changed(1);
1409 # Returns true if the item is at the current location
1410 # because it was transited there for a hold and the
1411 # hold has not been fulfilled
1412 sub checkin_check_holds_shelf {
1414 return 0 unless $self->copy;
1417 $U->copy_status($self->copy->status)->id ==
1418 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1420 # find the hold that put us on the holds shelf
1421 my $holds = $self->editor->search_action_hold_request(
1423 current_copy => $self->copy->id,
1424 capture_time => { '!=' => undef },
1425 fulfillment_time => undef,
1426 cancel_time => undef,
1431 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1432 $self->reshelve_copy(1);
1436 my $hold = $$holds[0];
1438 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1439 $hold->id. "] for copy ".$self->copy->barcode);
1441 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1442 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1446 $logger->info("circulator: hold is not for here..");
1447 $self->remote_hold($hold);
1452 sub checkin_handle_precat {
1454 my $copy = $self->copy;
1456 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1457 $copy->status(OILS_COPY_STATUS_CATALOGING);
1458 $self->update_copy();
1459 $self->checkin_changed(1);
1460 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1465 sub checkin_build_copy_transit {
1467 my $copy = $self->copy;
1468 my $transit = Fieldmapper::action::transit_copy->new;
1470 $transit->source($self->editor->requestor->ws_ou);
1471 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1472 $transit->target_copy($copy->id);
1473 $transit->source_send_time('now');
1474 $transit->copy_status( $U->copy_status($copy->status)->id );
1476 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1478 return $self->bail_on_events($self->editor->event)
1479 unless $self->editor->create_action_transit_copy($transit);
1481 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1483 $self->checkin_changed(1);
1487 sub attempt_checkin_hold_capture {
1489 my $copy = $self->copy;
1491 # See if this copy can fulfill any holds
1492 my ($hold) = $holdcode->find_nearest_permitted_hold(
1493 OpenSRF::AppSession->create('open-ils.storage'),
1494 $copy, $self->editor->requestor );
1497 $logger->debug("circulator: no potential permitted".
1498 "holds found for copy ".$copy->barcode);
1503 $logger->info("circulator: found permitted hold ".
1504 $hold->id . " for copy, capturing...");
1506 $hold->current_copy($copy->id);
1507 $hold->capture_time('now');
1509 # prevent DB errors caused by fetching
1510 # holds from storage, and updating through cstore
1511 $hold->clear_fulfillment_time;
1512 $hold->clear_fulfillment_staff;
1513 $hold->clear_fulfillment_lib;
1514 $hold->clear_expire_time;
1515 $hold->clear_cancel_time;
1516 $hold->clear_prev_check_time unless $hold->prev_check_time;
1518 $self->bail_on_events($self->editor->event)
1519 unless $self->editor->update_action_hold_request($hold);
1521 $self->checkin_changed(1);
1523 return 1 if $self->bail_out;
1525 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1527 # This hold was captured in the correct location
1528 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1529 $self->push_events(OpenILS::Event->new('SUCCESS'));
1531 #$self->do_hold_notify($hold->id);
1532 $self->notify_hold($hold->id);
1536 # Hold needs to be picked up elsewhere. Build a hold
1537 # transit and route the item.
1538 $self->checkin_build_hold_transit();
1539 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1540 return 1 if $self->bail_out;
1542 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1545 # make sure we save the copy status
1550 sub do_hold_notify {
1551 my( $self, $holdid ) = @_;
1553 $logger->info("circulator: running delayed hold notify process");
1555 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1556 hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1558 $logger->debug("circulator: built hold notifier");
1560 if(!$notifier->event) {
1562 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1564 my $stat = $notifier->send_email_notify;
1565 if( $stat == '1' ) {
1566 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1570 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1573 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1578 sub checkin_build_hold_transit {
1581 my $copy = $self->copy;
1582 my $hold = $self->hold;
1583 my $trans = Fieldmapper::action::hold_transit_copy->new;
1585 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1587 $trans->hold($hold->id);
1588 $trans->source($self->editor->requestor->ws_ou);
1589 $trans->dest($hold->pickup_lib);
1590 $trans->source_send_time("now");
1591 $trans->target_copy($copy->id);
1593 # when the copy gets to its destination, it will recover
1594 # this status - put it onto the holds shelf
1595 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1597 return $self->bail_on_events($self->editor->event)
1598 unless $self->editor->create_action_hold_transit_copy($trans);
1603 sub process_received_transit {
1605 my $copy = $self->copy;
1606 my $copyid = $self->copy->id;
1608 my $status_name = $U->copy_status($copy->status)->name;
1609 $logger->debug("circulator: attempting transit receive on ".
1610 "copy $copyid. Copy status is $status_name");
1612 my $transit = $self->transit;
1614 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1615 $logger->info("circulator: Fowarding transit on copy which is destined ".
1616 "for a different location. copy=$copyid,current ".
1617 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1619 return $self->bail_on_events(
1620 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1623 # The transit is received, set the receive time
1624 $transit->dest_recv_time('now');
1625 $self->bail_on_events($self->editor->event)
1626 unless $self->editor->update_action_transit_copy($transit);
1628 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1630 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1631 $copy->status( $transit->copy_status );
1632 $self->update_copy();
1633 return if $self->bail_out;
1637 #$self->do_hold_notify($hold_transit->hold);
1638 $self->notify_hold($hold_transit->hold);
1643 OpenILS::Event->new(
1646 payload => { transit => $transit, holdtransit => $hold_transit } ));
1648 return $hold_transit;
1652 sub checkin_handle_circ {
1656 my $circ = $self->circ;
1657 my $copy = $self->copy;
1661 # backdate the circ if necessary
1662 if($self->backdate) {
1663 $self->checkin_handle_backdate;
1664 return if $self->bail_out;
1667 if(!$circ->stop_fines) {
1668 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1669 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1670 $circ->stop_fines_time('now') unless $self->backdate;
1671 $circ->stop_fines_time($self->backdate) if $self->backdate;
1674 # see if there are any fines owed on this circ. if not, close it
1675 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1676 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1678 # Set the checkin vars since we have the item
1679 $circ->checkin_time('now');
1680 $circ->checkin_staff($self->editor->requestor->id);
1681 $circ->checkin_lib($self->editor->requestor->ws_ou);
1683 my $circ_lib = (ref $self->copy->circ_lib) ?
1684 $self->copy->circ_lib->id : $self->copy->circ_lib;
1685 my $stat = $U->copy_status($self->copy->status)->id;
1687 # If the item is lost/missing and it needs to be sent home, don't
1688 # reshelve the copy, leave it lost/missing so the recipient will know
1689 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1690 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1691 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1694 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1699 return $self->bail_on_events($self->editor->event)
1700 unless $self->editor->update_action_circulation($circ);
1704 sub checkin_handle_backdate {
1707 my $bd = $self->backdate;
1708 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1709 $bd = "${bd}T23:59:59";
1711 my $bills = $self->editor->search_money_billing(
1713 billing_ts => { '>=' => $bd },
1714 xact => $self->circ->id,
1715 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1719 for my $bill (@$bills) {
1720 if( !$bill->voided or $bill->voided =~ /f/i ) {
1722 my $n = $bill->note || "";
1723 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1725 $self->bail_on_events($self->editor->event)
1726 unless $self->editor->update_money_billing($bill);
1733 # XXX Legacy version for Circ.pm support
1734 sub _checkin_handle_backdate {
1735 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1738 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1739 $bd = "${bd}T23:59:59";
1742 my $bills = $session->request(
1743 "open-ils.storage.direct.money.billing.search_where.atomic",
1744 billing_ts => { '>=' => $bd },
1746 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1750 for my $bill (@$bills) {
1752 my $n = $bill->note || "";
1753 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1754 my $s = $session->request(
1755 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1756 return $U->DB_UPDATE_FAILED($bill) unless $s;
1766 sub find_patron_from_copy {
1768 my $circs = $self->editor->search_action_circulation(
1769 { target_copy => $self->copy->id, checkin_time => undef });
1770 my $circ = $circs->[0];
1771 return unless $circ;
1772 my $u = $self->editor->retrieve_actor_user($circ->usr)
1773 or return $self->bail_on_events($self->editor->event);
1777 sub check_checkin_copy_status {
1779 my $copy = $self->copy;
1785 my $status = $U->copy_status($copy->status)->id;
1788 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1789 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1790 $status == OILS_COPY_STATUS_IN_PROCESS ||
1791 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1792 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1793 $status == OILS_COPY_STATUS_CATALOGING ||
1794 $status == OILS_COPY_STATUS_RESHELVING );
1796 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1797 if( $status == OILS_COPY_STATUS_LOST );
1799 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1800 if( $status == OILS_COPY_STATUS_MISSING );
1802 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1807 # --------------------------------------------------------------------------
1808 # On checkin, we need to return as many relevant objects as we can
1809 # --------------------------------------------------------------------------
1810 sub checkin_flesh_events {
1813 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1814 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1815 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1819 for my $evt (@{$self->events}) {
1822 $payload->{copy} = $U->unflesh_copy($self->copy);
1823 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1824 $payload->{circ} = $self->circ;
1825 $payload->{transit} = $self->transit;
1826 $payload->{hold} = $self->hold;
1828 $evt->{payload} = $payload;
1833 my( $self, $msg ) = @_;
1834 my $bc = ($self->copy) ? $self->copy->barcode :
1837 my $usr = ($self->patron) ? $self->patron->id : "";
1838 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1839 ", recipient=$usr, copy=$bc");
1845 $self->log_me("do_renew()");
1846 $self->is_renewal(1);
1848 unless( $self->is_renewal ) {
1849 return $self->bail_on_events($self->editor->events)
1850 unless $self->editor->allowed('RENEW_CIRC');
1853 # Make sure there is an open circ to renew that is not
1854 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1855 my $circ = $self->editor->search_action_circulation(
1856 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1859 $circ = $self->editor->search_action_circulation(
1861 target_copy => $self->copy->id,
1862 stop_fines => OILS_STOP_FINES_MAX_FINES,
1863 checkin_time => undef
1868 return $self->bail_on_events($self->editor->event) unless $circ;
1870 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1871 if $circ->renewal_remaining < 1;
1873 # -----------------------------------------------------------------
1875 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1878 $self->run_renew_permit;
1881 $self->do_checkin();
1882 return if $self->bail_out;
1884 unless( $self->permit_override ) {
1886 return if $self->bail_out;
1887 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1888 $self->remove_event('ITEM_NOT_CATALOGED');
1891 $self->override_events;
1892 return if $self->bail_out;
1895 $self->do_checkout();
1900 my( $self, $evt ) = @_;
1901 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1902 $logger->debug("circulator: removing event from list: $evt");
1903 my @events = @{$self->events};
1904 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1909 my( $self, $evt ) = @_;
1910 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1911 return grep { $_->{textcode} eq $evt } @{$self->events};
1916 sub run_renew_permit {
1918 my $runner = $self->script_runner;
1920 $runner->load($self->circ_permit_renew);
1921 my $result = $runner->run or
1922 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1923 my $events = $result->{events};
1925 $logger->activity("ciculator: circ_permit_renew for user ".
1926 $self->patron->id." returned events: @$events") if @$events;
1928 $self->push_events(OpenILS::Event->new($_)) for @$events;
1930 $logger->debug("circulator: re-creating script runner to be safe");
1931 $self->mk_script_runner;