1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenSRF::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
15 my $conf = OpenSRF::Utils::SettingsClient->new;
16 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
17 my @pfx = ( @pfx2, "scripts" );
19 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
20 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
21 my $d = $conf->config_value( @pfx, 'circ_duration' );
22 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
23 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
24 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
25 my $lb = $conf->config_value( @pfx2, 'script_path' );
27 $logger->error( "Missing circ script(s)" )
28 unless( $p and $c and $d and $f and $m and $pr );
30 $scripts{circ_permit_patron} = $p;
31 $scripts{circ_permit_copy} = $c;
32 $scripts{circ_duration} = $d;
33 $scripts{circ_recurring_fines}= $f;
34 $scripts{circ_max_fines} = $m;
35 $scripts{circ_permit_renew} = $pr;
37 $lb = [ $lb ] unless ref($lb);
41 "circulator: Loaded rules scripts for circ: " .
42 "circ permit patron = $p, ".
43 "circ permit copy = $c, ".
44 "circ duration = $d, ".
45 "circ recurring fines = $f, " .
46 "circ max fines = $m, ".
47 "circ renew permit = $pr. ".
52 __PACKAGE__->register_method(
53 method => "run_method",
54 api_name => "open-ils.circ.checkout.permit",
56 Determines if the given checkout can occur
57 @param authtoken The login session key
58 @param params A trailing hash of named params including
59 barcode : The copy barcode,
60 patron : The patron the checkout is occurring for,
61 renew : true or false - whether or not this is a renewal
62 @return The event that occurred during the permit check.
66 __PACKAGE__->register_method (
67 method => 'run_method',
68 api_name => 'open-ils.circ.checkout.permit.override',
69 signature => q/@see open-ils.circ.checkout.permit/,
73 __PACKAGE__->register_method(
74 method => "run_method",
75 api_name => "open-ils.circ.checkout",
78 @param authtoken The login session key
79 @param params A named hash of params including:
81 barcode If no copy is provided, the copy is retrieved via barcode
82 copyid If no copy or barcode is provide, the copy id will be use
83 patron The patron's id
84 noncat True if this is a circulation for a non-cataloted item
85 noncat_type The non-cataloged type id
86 noncat_circ_lib The location for the noncat circ.
87 precat The item has yet to be cataloged
88 dummy_title The temporary title of the pre-cataloded item
89 dummy_author The temporary authr of the pre-cataloded item
90 Default is the home org of the staff member
91 @return The SUCCESS event on success, any other event depending on the error
94 __PACKAGE__->register_method(
95 method => "run_method",
96 api_name => "open-ils.circ.checkin",
99 Generic super-method for handling all copies
100 @param authtoken The login session key
101 @param params Hash of named parameters including:
102 barcode - The copy barcode
103 force - If true, copies in bad statuses will be checked in and give good statuses
108 __PACKAGE__->register_method(
109 method => "run_method",
110 api_name => "open-ils.circ.checkin.override",
111 signature => q/@see open-ils.circ.checkin/
114 __PACKAGE__->register_method(
115 method => "run_method",
116 api_name => "open-ils.circ.renew.override",
117 signature => q/@see open-ils.circ.renew/,
121 __PACKAGE__->register_method(
122 method => "run_method",
123 api_name => "open-ils.circ.renew",
124 notes => <<" NOTES");
125 PARAMS( authtoken, circ => circ_id );
126 open-ils.circ.renew(login_session, circ_object);
127 Renews the provided circulation. login_session is the requestor of the
128 renewal and if the logged in user is not the same as circ->usr, then
129 the logged in user must have RENEW_CIRC permissions.
132 __PACKAGE__->register_method(
133 method => "run_method",
134 api_name => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136 method => "run_method",
137 api_name => "open-ils.circ.checkout.full.override");
142 my( $self, $conn, $auth, $args ) = @_;
143 translate_legacy_args($args);
144 my $api = $self->api_name;
147 OpenILS::Application::Circ::Circulator->new($auth, %$args);
149 return circ_events($circulator) if $circulator->bail_out;
151 # --------------------------------------------------------------------------
152 # Go ahead and load the script runner to make sure we have all
153 # of the objects we need
154 # --------------------------------------------------------------------------
155 $circulator->is_renewal(1) if $api =~ /renew/;
156 $circulator->is_checkin(1) if $api =~ /checkin/;
157 $circulator->mk_script_runner;
158 return circ_events($circulator) if $circulator->bail_out;
160 $circulator->circ_permit_patron($scripts{circ_permit_patron});
161 $circulator->circ_permit_copy($scripts{circ_permit_copy});
162 $circulator->circ_duration($scripts{circ_duration});
163 $circulator->circ_permit_renew($scripts{circ_permit_renew});
165 $circulator->override(1) if $api =~ /override/o;
167 if( $api =~ /checkout\.permit/ ) {
168 $circulator->do_permit();
170 } elsif( $api =~ /checkout.full/ ) {
172 $circulator->do_permit();
173 unless( $circulator->bail_out ) {
174 $circulator->events([]);
175 $circulator->do_checkout();
178 } elsif( $api =~ /checkout/ ) {
179 $circulator->do_checkout();
181 } elsif( $api =~ /checkin/ ) {
182 $circulator->do_checkin();
184 } elsif( $api =~ /renew/ ) {
185 $circulator->is_renewal(1);
186 $circulator->do_renew();
189 if( $circulator->bail_out ) {
192 # make sure no success event accidentally slip in
194 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
197 my @e = @{$circulator->events};
198 push( @ee, $_->{textcode} ) for @e;
199 $logger->info("circulator: bailing out with events: @ee");
201 $circulator->editor->rollback;
204 $circulator->editor->commit;
207 $circulator->script_runner->cleanup;
209 $conn->respond_complete(circ_events($circulator));
211 unless($circulator->bail_out) {
212 $circulator->do_hold_notify($circulator->notify_hold)
213 if $circulator->notify_hold;
219 my @e = @{$circ->events};
220 # if we have multiple events, SUCCESS should not be one of them;
221 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
222 return (@e == 1) ? $e[0] : \@e;
227 sub translate_legacy_args {
230 if( $$args{barcode} ) {
231 $$args{copy_barcode} = $$args{barcode};
232 delete $$args{barcode};
235 if( $$args{copyid} ) {
236 $$args{copy_id} = $$args{copyid};
237 delete $$args{copyid};
240 if( $$args{patronid} ) {
241 $$args{patron_id} = $$args{patronid};
242 delete $$args{patronid};
245 if( $$args{patron} and !ref($$args{patron}) ) {
246 $$args{patron_id} = $$args{patron};
247 delete $$args{patron};
251 if( $$args{noncat} ) {
252 $$args{is_noncat} = $$args{noncat};
253 delete $$args{noncat};
256 if( $$args{precat} ) {
257 $$args{is_precat} = $$args{precat};
258 delete $$args{precat};
264 # --------------------------------------------------------------------------
265 # This package actually manages all of the circulation logic
266 # --------------------------------------------------------------------------
267 package OpenILS::Application::Circ::Circulator;
268 use strict; use warnings;
269 use vars q/$AUTOLOAD/;
271 use OpenILS::Utils::Fieldmapper;
272 use OpenSRF::Utils::Cache;
273 use Digest::MD5 qw(md5_hex);
274 use DateTime::Format::ISO8601;
275 use OpenILS::Utils::PermitHold;
276 use OpenSRF::Utils qw/:datetime/;
277 use OpenSRF::Utils::SettingsClient;
278 use OpenILS::Application::Circ::Holds;
279 use OpenILS::Application::Circ::Transit;
280 use OpenSRF::Utils::Logger qw(:logger);
281 use OpenILS::Utils::CStoreEditor qw/:funcs/;
282 use OpenILS::Application::Circ::ScriptBuilder;
283 use OpenILS::Const qw/:const/;
285 my $U = "OpenILS::Application::AppUtils";
286 my $holdcode = "OpenILS::Application::Circ::Holds";
287 my $transcode = "OpenILS::Application::Circ::Transit";
292 # --------------------------------------------------------------------------
293 # Add a pile of automagic getter/setter methods
294 # --------------------------------------------------------------------------
295 my @AUTOLOAD_FIELDS = qw/
336 recurring_fines_level
354 my $type = ref($self) or die "$self is not an object";
356 my $name = $AUTOLOAD;
359 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
360 $logger->error("circulator: $type: invalid autoload field: $name");
361 die "$type: invalid autoload field: $name\n"
366 *{"${type}::${name}"} = sub {
369 $s->{$name} = $v if defined $v;
373 return $self->$name($data);
378 my( $class, $auth, %args ) = @_;
379 $class = ref($class) || $class;
380 my $self = bless( {}, $class );
384 new_editor(xact => 1, authtoken => $auth) );
386 unless( $self->editor->checkauth ) {
387 $self->bail_on_events($self->editor->event);
391 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
393 $self->$_($args{$_}) for keys %args;
396 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
402 # --------------------------------------------------------------------------
403 # True if we should discontinue processing
404 # --------------------------------------------------------------------------
406 my( $self, $bool ) = @_;
407 if( defined $bool ) {
408 $logger->info("circulator: BAILING OUT") if $bool;
409 $self->{bail_out} = $bool;
411 return $self->{bail_out};
416 my( $self, @evts ) = @_;
419 $logger->info("circulator: pushing event ".$e->{textcode});
420 push( @{$self->events}, $e ) unless
421 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
427 my $key = md5_hex( time() . rand() . "$$" );
428 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
429 return $self->permit_key($key);
432 sub check_permit_key {
434 my $key = $self->permit_key;
435 return 0 unless $key;
436 my $k = "oils_permit_key_$key";
437 my $one = $self->cache_handle->get_cache($k);
438 $self->cache_handle->delete_cache($k);
439 return ($one) ? 1 : 0;
443 # --------------------------------------------------------------------------
444 # This builds the script runner environment and fetches most of the
446 # --------------------------------------------------------------------------
447 sub mk_script_runner {
453 qw/copy copy_barcode copy_id patron
454 patron_id patron_barcode volume title editor/;
456 # Translate our objects into the ScriptBuilder args hash
457 $$args{$_} = $self->$_() for @fields;
459 $args->{ignore_user_status} = 1 if $self->is_checkin;
460 $$args{fetch_patron_by_circ_copy} = 1;
461 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
463 if( my $pco = $self->pending_checkouts ) {
464 $logger->info("circulator: we were given a pending checkouts number of $pco");
465 $$args{patronItemsOut} = $pco;
468 # This fetches most of the objects we need
469 $self->script_runner(
470 OpenILS::Application::Circ::ScriptBuilder->build($args));
472 # Now we translate the ScriptBuilder objects back into self
473 $self->$_($$args{$_}) for @fields;
475 my @evts = @{$args->{_events}} if $args->{_events};
477 $logger->debug("circulator: script builder returned events: @evts") if @evts;
481 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
482 if(!$self->is_noncat and
484 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
488 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
489 return $self->bail_on_events(@e);
493 $self->is_precat(1) if $self->copy
494 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
496 # We can't renew if there is no copy
497 return $self->bail_on_events(@evts) if
498 $self->is_renewal and !$self->copy;
500 # Set some circ-specific flags in the script environment
501 my $evt = "environment";
502 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
504 if( $self->is_noncat ) {
505 $self->script_runner->insert("$evt.isNonCat", 1);
506 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
509 if( $self->is_precat ) {
510 $self->script_runner->insert("environment.isPrecat", 1, 1);
513 $self->script_runner->add_path( $_ ) for @$script_libs;
521 # --------------------------------------------------------------------------
522 # Does the circ permit work
523 # --------------------------------------------------------------------------
527 $self->log_me("do_permit()");
529 unless( $self->editor->requestor->id == $self->patron->id ) {
530 return $self->bail_on_events($self->editor->event)
531 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
534 $self->check_captured_holds();
535 $self->do_copy_checks();
536 return if $self->bail_out;
537 $self->run_patron_permit_scripts();
538 $self->run_copy_permit_scripts()
539 unless $self->is_precat or $self->is_noncat;
540 $self->override_events() unless $self->is_renewal;
541 return if $self->bail_out;
543 if( $self->is_precat ) {
546 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
547 return $self->bail_out(1) unless $self->is_renewal;
553 payload => $self->mk_permit_key));
557 sub check_captured_holds {
559 my $copy = $self->copy;
560 my $patron = $self->patron;
562 return undef unless $copy;
564 my $s = $U->copy_status($copy->status)->id;
565 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
566 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
568 # Item is on the holds shelf, make sure it's going to the right person
569 my $holds = $self->editor->search_action_hold_request(
572 current_copy => $copy->id ,
573 capture_time => { '!=' => undef },
574 cancel_time => undef,
575 fulfillment_time => undef
581 if( $holds and $$holds[0] ) {
582 return undef if $$holds[0]->usr == $patron->id;
585 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
587 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
593 my $copy = $self->copy;
596 my $stat = $U->copy_status($copy->status)->id;
598 # We cannot check out a copy if it is in-transit
599 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
600 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
603 $self->handle_claims_returned();
604 return if $self->bail_out;
606 # no claims returned circ was found, check if there is any open circ
607 unless( $self->is_renewal ) {
608 my $circs = $self->editor->search_action_circulation(
609 { target_copy => $copy->id, checkin_time => undef }
612 return $self->bail_on_events(
613 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
618 sub send_penalty_request {
620 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
621 $self->penalty_request(
623 'open-ils.penalty.patron_penalty.calculate',
625 authtoken => $self->editor->authtoken,
626 patron => $self->patron } ) );
629 sub gather_penalty_request {
631 return [] unless $self->penalty_request;
632 my $data = $self->penalty_request->recv;
634 throw $data if UNIVERSAL::isa($data,'Error');
635 $data = $data->content;
636 return $data->{fatal_penalties};
638 $logger->error("circulator: penalty request returned no data");
642 # ---------------------------------------------------------------------
643 # This pushes any patron-related events into the list but does not
644 # set bail_out for any events
645 # ---------------------------------------------------------------------
646 sub run_patron_permit_scripts {
648 my $runner = $self->script_runner;
649 my $patronid = $self->patron->id;
651 $self->send_penalty_request() unless $self->is_renewal;
653 # ---------------------------------------------------------------------
654 # Now run the patron permit script
655 # ---------------------------------------------------------------------
656 $runner->load($self->circ_permit_patron);
657 my $result = $runner->run or
658 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
660 my $patron_events = $result->{events};
664 # ---------------------------------------------------------------------
665 # this is policy directly in the code, not a good idea in general, but
666 # the penalty server doesn't know anything about renewals, so we
667 # have to strip the event out here
668 my $penalties = ($self->is_renewal) ? [] : $self->gather_penalty_request();
669 # ---------------------------------------------------------------------
671 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
673 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
675 $self->push_events(@allevents);
679 sub run_copy_permit_scripts {
681 my $copy = $self->copy || return;
682 my $runner = $self->script_runner;
684 # ---------------------------------------------------------------------
685 # Capture all of the copy permit events
686 # ---------------------------------------------------------------------
687 $runner->load($self->circ_permit_copy);
688 my $result = $runner->run or
689 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
690 my $copy_events = $result->{events};
692 # ---------------------------------------------------------------------
693 # Now collect all of the events together
694 # ---------------------------------------------------------------------
696 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
698 # See if this copy has an alert message
699 my $ae = $self->check_copy_alert();
700 push( @allevents, $ae ) if $ae;
702 # uniquify the events
703 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
704 @allevents = values %hash;
707 $_->{payload} = $copy if
708 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
711 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
713 $self->push_events(@allevents);
717 sub check_copy_alert {
719 return undef if $self->is_renewal;
720 return OpenILS::Event->new(
721 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
722 if $self->copy and $self->copy->alert_message;
728 # --------------------------------------------------------------------------
729 # If the call is overriding and has permissions to override every collected
730 # event, the are cleared. Any event that the caller does not have
731 # permission to override, will be left in the event list and bail_out will
733 # XXX We need code in here to cancel any holds/transits on copies
734 # that are being force-checked out
735 # --------------------------------------------------------------------------
736 sub override_events {
738 my @events = @{$self->events};
739 return unless @events;
741 if(!$self->override) {
742 return $self->bail_out(1)
743 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
748 for my $e (@events) {
749 my $tc = $e->{textcode};
750 next if $tc eq 'SUCCESS';
751 my $ov = "$tc.override";
752 $logger->info("circulator: attempting to override event: $ov");
754 return $self->bail_on_events($self->editor->event)
755 unless( $self->editor->allowed($ov) );
760 # --------------------------------------------------------------------------
761 # If there is an open claimsreturn circ on the requested copy, close the
762 # circ if overriding, otherwise bail out
763 # --------------------------------------------------------------------------
764 sub handle_claims_returned {
766 my $copy = $self->copy;
768 my $CR = $self->editor->search_action_circulation(
770 target_copy => $copy->id,
771 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
772 checkin_time => undef,
776 return unless ($CR = $CR->[0]);
780 # - If the caller has set the override flag, we will check the item in
781 if($self->override) {
783 $CR->checkin_time('now');
784 $CR->checkin_lib($self->editor->requestor->ws_ou);
785 $CR->checkin_staff($self->editor->requestor->id);
787 $evt = $self->editor->event
788 unless $self->editor->update_action_circulation($CR);
791 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
794 $self->bail_on_events($evt) if $evt;
799 # --------------------------------------------------------------------------
800 # This performs the checkout
801 # --------------------------------------------------------------------------
805 $self->log_me("do_checkout()");
807 # make sure perms are good if this isn't a renewal
808 unless( $self->is_renewal ) {
809 return $self->bail_on_events($self->editor->event)
810 unless( $self->editor->allowed('COPY_CHECKOUT') );
813 # verify the permit key
814 unless( $self->check_permit_key ) {
815 if( $self->permit_override ) {
816 return $self->bail_on_events($self->editor->event)
817 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
819 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
823 # if this is a non-cataloged circ, build the circ and finish
824 if( $self->is_noncat ) {
825 $self->checkout_noncat;
827 OpenILS::Event->new('SUCCESS',
828 payload => { noncat_circ => $self->circ }));
832 if( $self->is_precat ) {
833 $self->script_runner->insert("environment.isPrecat", 1, 1);
834 $self->make_precat_copy;
835 return if $self->bail_out;
837 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
838 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
841 $self->do_copy_checks;
842 return if $self->bail_out;
844 $self->run_checkout_scripts();
845 return if $self->bail_out;
847 $self->build_checkout_circ_object();
848 return if $self->bail_out;
850 $self->apply_modified_due_date();
851 return if $self->bail_out;
853 return $self->bail_on_events($self->editor->event)
854 unless $self->editor->create_action_circulation($self->circ);
856 # refresh the circ to force local time zone for now
857 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
859 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
861 return if $self->bail_out;
863 $self->handle_checkout_holds();
864 return if $self->bail_out;
866 # ------------------------------------------------------------------------------
867 # Update the patron penalty info in the DB. Run it for permit-overrides or
868 # renewals since both of those cases do not require the penalty server to
869 # run during the permit phase of the checkout
870 # ------------------------------------------------------------------------------
871 if( $self->permit_override or $self->is_renewal ) {
872 $U->update_patron_penalties(
873 authtoken => $self->editor->authtoken,
874 patron => $self->patron,
879 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
881 OpenILS::Event->new('SUCCESS',
883 copy => $U->unflesh_copy($self->copy),
886 holds_fulfilled => $self->fulfilled_holds,
894 my $copy = $self->copy;
896 my $stat = $copy->status if ref $copy->status;
897 my $loc = $copy->location if ref $copy->location;
898 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
900 $copy->status($stat->id) if $stat;
901 $copy->location($loc->id) if $loc;
902 $copy->circ_lib($circ_lib->id) if $circ_lib;
903 $copy->editor($self->editor->requestor->id);
904 $copy->edit_date('now');
905 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
907 return $self->bail_on_events($self->editor->event)
908 unless $self->editor->update_asset_copy($self->copy);
910 $copy->status($U->copy_status($copy->status));
911 $copy->location($loc) if $loc;
912 $copy->circ_lib($circ_lib) if $circ_lib;
917 my( $self, @evts ) = @_;
918 $self->push_events(@evts);
922 sub handle_checkout_holds {
925 my $copy = $self->copy;
926 my $patron = $self->patron;
928 my $holds = $self->editor->search_action_hold_request(
930 current_copy => $copy->id ,
931 cancel_time => undef,
932 fulfillment_time => undef
938 # XXX We should only fulfill one hold here...
939 # XXX If a hold was transited to the user who is checking out
940 # the item, we need to make sure that hold is what's grabbed
943 # for now, just sort by id to get what should be the oldest hold
944 $holds = [ sort { $a->id <=> $b->id } @$holds ];
945 my @myholds = grep { $_->usr eq $patron->id } @$holds;
946 my @altholds = grep { $_->usr ne $patron->id } @$holds;
949 my $hold = $myholds[0];
951 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
953 # if the hold was never officially captured, capture it.
954 $hold->capture_time('now') unless $hold->capture_time;
956 # just make sure it's set correctly
957 $hold->current_copy($copy->id);
959 $hold->fulfillment_time('now');
960 $hold->fulfillment_staff($self->editor->requestor->id);
961 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
963 return $self->bail_on_events($self->editor->event)
964 unless $self->editor->update_action_hold_request($hold);
966 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
968 push( @fulfilled, $hold->id );
971 # If there are any holds placed for other users that point to this copy,
972 # then we need to un-target those holds so the targeter can pick a new copy
975 $logger->info("circulator: un-targeting hold ".$_->id.
976 " because copy ".$copy->id." is getting checked out");
978 # - make the targeter process this hold at next run
979 $_->clear_prev_check_time;
981 # - clear out the targetted copy
982 $_->clear_current_copy;
983 $_->clear_capture_time;
985 return $self->bail_on_event($self->editor->event)
986 unless $self->editor->update_action_hold_request($_);
990 $self->fulfilled_holds(\@fulfilled);
995 sub run_checkout_scripts {
999 my $runner = $self->script_runner;
1000 $runner->load($self->circ_duration);
1002 my $result = $runner->run or
1003 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1005 my $duration = $result->{durationRule};
1006 my $recurring = $result->{recurringFinesRule};
1007 my $max_fine = $result->{maxFine};
1009 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1011 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1012 return $self->bail_on_events($evt) if $evt;
1014 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1015 return $self->bail_on_events($evt) if $evt;
1017 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1018 return $self->bail_on_events($evt) if $evt;
1022 # The item circulates with an unlimited duration
1028 $self->duration_rule($duration);
1029 $self->recurring_fines_rule($recurring);
1030 $self->max_fine_rule($max_fine);
1034 sub build_checkout_circ_object {
1037 my $circ = Fieldmapper::action::circulation->new;
1038 my $duration = $self->duration_rule;
1039 my $max = $self->max_fine_rule;
1040 my $recurring = $self->recurring_fines_rule;
1041 my $copy = $self->copy;
1042 my $patron = $self->patron;
1046 my $dname = $duration->name;
1047 my $mname = $max->name;
1048 my $rname = $recurring->name;
1050 $logger->debug("circulator: building circulation ".
1051 "with duration=$dname, maxfine=$mname, recurring=$rname");
1053 $circ->duration( $duration->shrt )
1054 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1055 $circ->duration( $duration->normal )
1056 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1057 $circ->duration( $duration->extended )
1058 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1060 $circ->recuring_fine( $recurring->low )
1061 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1062 $circ->recuring_fine( $recurring->normal )
1063 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1064 $circ->recuring_fine( $recurring->high )
1065 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1067 $circ->duration_rule( $duration->name );
1068 $circ->recuring_fine_rule( $recurring->name );
1069 $circ->max_fine_rule( $max->name );
1070 $circ->max_fine( $max->amount );
1072 $circ->fine_interval($recurring->recurance_interval);
1073 $circ->renewal_remaining( $duration->max_renewals );
1077 $logger->info("circulator: copy found with an unlimited circ duration");
1078 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1079 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1080 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1081 $circ->renewal_remaining(0);
1084 $circ->target_copy( $copy->id );
1085 $circ->usr( $patron->id );
1086 $circ->circ_lib( $self->circ_lib );
1088 if( $self->is_renewal ) {
1089 $circ->opac_renewal(1);
1090 $circ->renewal_remaining($self->renewal_remaining);
1091 $circ->circ_staff($self->editor->requestor->id);
1094 # if the user provided an overiding checkout time,
1095 # (e.g. the checkout really happened several hours ago), then
1096 # we apply that here. Does this need a perm??
1097 $circ->xact_start(clense_ISO8601($self->checkout_time))
1098 if $self->checkout_time;
1100 # if a patron is renewing, 'requestor' will be the patron
1101 $circ->circ_staff($self->editor->requestor->id);
1102 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1108 sub apply_modified_due_date {
1110 my $circ = $self->circ;
1111 my $copy = $self->copy;
1113 if( $self->due_date ) {
1115 return $self->bail_on_events($self->editor->event)
1116 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1118 $circ->due_date(clense_ISO8601($self->due_date));
1122 # if the due_date lands on a day when the location is closed
1123 return unless $copy and $circ->due_date;
1125 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1127 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1128 " with an item due date of ".$circ->due_date );
1130 my $dateinfo = $U->storagereq(
1131 'open-ils.storage.actor.org_unit.closed_date.overlap',
1132 $org, $circ->due_date );
1135 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1136 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1138 # XXX make the behavior more dynamic
1139 # for now, we just push the due date to after the close date
1140 $circ->due_date($dateinfo->{end});
1147 sub create_due_date {
1148 my( $self, $duration ) = @_;
1149 my ($sec,$min,$hour,$mday,$mon,$year) =
1150 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1151 $year += 1900; $mon += 1;
1152 my $due_date = sprintf(
1153 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1154 $year, $mon, $mday, $hour, $min, $sec);
1160 sub make_precat_copy {
1162 my $copy = $self->copy;
1165 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1167 $copy->editor($self->editor->requestor->id);
1168 $copy->edit_date('now');
1169 $copy->dummy_title($self->dummy_title);
1170 $copy->dummy_author($self->dummy_author);
1172 $self->update_copy();
1176 $logger->info("circulator: Creating a new precataloged ".
1177 "copy in checkout with barcode " . $self->copy_barcode);
1179 $copy = Fieldmapper::asset::copy->new;
1180 $copy->circ_lib($self->circ_lib);
1181 $copy->creator($self->editor->requestor->id);
1182 $copy->editor($self->editor->requestor->id);
1183 $copy->barcode($self->copy_barcode);
1184 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1185 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1186 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1188 $copy->dummy_title($self->dummy_title || "");
1189 $copy->dummy_author($self->dummy_author || "");
1191 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1193 $self->push_events($self->editor->event);
1197 # this is a little bit of a hack, but we need to
1198 # get the copy into the script runner
1199 $self->script_runner->insert("environment.copy", $copy, 1);
1203 sub checkout_noncat {
1209 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1210 my $count = $self->noncat_count || 1;
1211 my $cotime = clense_ISO8601($self->checkout_time) || "";
1213 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1217 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1218 $self->editor->requestor->id,
1226 $self->push_events($evt);
1237 $self->log_me("do_checkin()");
1240 return $self->bail_on_events(
1241 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1244 if( $self->checkin_check_holds_shelf() ) {
1245 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1246 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1247 $self->checkin_flesh_events;
1251 unless( $self->is_renewal ) {
1252 return $self->bail_on_events($self->editor->event)
1253 unless $self->editor->allowed('COPY_CHECKIN');
1256 $self->push_events($self->check_copy_alert());
1257 $self->push_events($self->check_checkin_copy_status());
1259 # the renew code will have already found our circulation object
1260 unless( $self->is_renewal and $self->circ ) {
1262 $self->editor->search_action_circulation(
1263 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1266 # if the circ is marked as 'claims returned', add the event to the list
1267 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1268 if ($self->circ and $self->circ->stop_fines
1269 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1271 # handle the overridable events
1272 $self->override_events unless $self->is_renewal;
1273 return if $self->bail_out;
1277 $self->editor->search_action_transit_copy(
1278 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1282 $self->checkin_handle_circ;
1283 return if $self->bail_out;
1284 $self->checkin_changed(1);
1286 } elsif( $self->transit ) {
1287 my $hold_transit = $self->process_received_transit;
1288 $self->checkin_changed(1);
1290 if( $self->bail_out ) {
1291 $self->checkin_flesh_events;
1295 if( my $e = $self->check_checkin_copy_status() ) {
1296 # If the original copy status is special, alert the caller
1297 my $ev = $self->events;
1298 $self->events([$e]);
1299 $self->override_events;
1300 return if $self->bail_out;
1304 if( $hold_transit or
1305 $U->copy_status($self->copy->status)->id
1306 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1309 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1310 $U->fetch_open_hold_by_copy($self->copy->id)
1313 $self->checkin_flesh_events;
1317 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1318 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1319 " that is in-transit, but there is no transit.. repairing");
1320 $self->reshelve_copy(1);
1321 return if $self->bail_out;
1324 if( $self->is_renewal ) {
1325 $self->push_events(OpenILS::Event->new('SUCCESS'));
1329 # ------------------------------------------------------------------------------
1330 # Circulations and transits are now closed where necessary. Now go on to see if
1331 # this copy can fulfill a hold or needs to be routed to a different location
1332 # ------------------------------------------------------------------------------
1334 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1335 return if $self->bail_out;
1337 } else { # not needed for a hold
1340 my $circ_lib = (ref $self->copy->circ_lib) ?
1341 $self->copy->circ_lib->id : $self->copy->circ_lib;
1343 if( $self->remote_hold ) {
1344 $circ_lib = $self->remote_hold->pickup_lib;
1345 $logger->warn("circulator: Copy ".$self->copy->barcode.
1346 " is on a remote hold's shelf, sending to $circ_lib");
1349 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1351 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1353 $self->checkin_handle_precat();
1354 return if $self->bail_out;
1358 my $bc = $self->copy->barcode;
1359 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1360 $self->checkin_build_copy_transit($circ_lib);
1361 return if $self->bail_out;
1362 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1366 $self->reshelve_copy;
1367 return if $self->bail_out;
1369 unless($self->checkin_changed) {
1371 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1372 my $stat = $U->copy_status($self->copy->status)->id;
1374 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1375 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1376 $self->bail_out(1); # no need to commit anything
1379 $self->push_events(OpenILS::Event->new('SUCCESS'))
1380 unless @{$self->events};
1384 # ------------------------------------------------------------------------------
1385 # Update the patron penalty info in the DB
1386 # ------------------------------------------------------------------------------
1387 $U->update_patron_penalties(
1388 authtoken => $self->editor->authtoken,
1389 patron => $self->patron,
1390 background => 1 ) if $self->is_checkin;
1392 $self->checkin_flesh_events;
1398 my $force = $self->force || shift;
1399 my $copy = $self->copy;
1401 my $stat = $U->copy_status($copy->status)->id;
1404 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1405 $stat != OILS_COPY_STATUS_CATALOGING and
1406 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1407 $stat != OILS_COPY_STATUS_RESHELVING )) {
1409 $copy->status( OILS_COPY_STATUS_RESHELVING );
1411 $self->checkin_changed(1);
1416 # Returns true if the item is at the current location
1417 # because it was transited there for a hold and the
1418 # hold has not been fulfilled
1419 sub checkin_check_holds_shelf {
1421 return 0 unless $self->copy;
1424 $U->copy_status($self->copy->status)->id ==
1425 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1427 # find the hold that put us on the holds shelf
1428 my $holds = $self->editor->search_action_hold_request(
1430 current_copy => $self->copy->id,
1431 capture_time => { '!=' => undef },
1432 fulfillment_time => undef,
1433 cancel_time => undef,
1438 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1439 $self->reshelve_copy(1);
1443 my $hold = $$holds[0];
1445 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1446 $hold->id. "] for copy ".$self->copy->barcode);
1448 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1449 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1453 $logger->info("circulator: hold is not for here..");
1454 $self->remote_hold($hold);
1459 sub checkin_handle_precat {
1461 my $copy = $self->copy;
1463 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1464 $copy->status(OILS_COPY_STATUS_CATALOGING);
1465 $self->update_copy();
1466 $self->checkin_changed(1);
1467 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1472 sub checkin_build_copy_transit {
1475 my $copy = $self->copy;
1476 my $transit = Fieldmapper::action::transit_copy->new;
1478 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1479 $logger->info("circulator: transiting copy to $dest");
1481 $transit->source($self->editor->requestor->ws_ou);
1482 $transit->dest($dest);
1483 $transit->target_copy($copy->id);
1484 $transit->source_send_time('now');
1485 $transit->copy_status( $U->copy_status($copy->status)->id );
1487 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1489 return $self->bail_on_events($self->editor->event)
1490 unless $self->editor->create_action_transit_copy($transit);
1492 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1494 $self->checkin_changed(1);
1498 sub attempt_checkin_hold_capture {
1500 my $copy = $self->copy;
1502 # See if this copy can fulfill any holds
1503 my ($hold) = $holdcode->find_nearest_permitted_hold(
1504 OpenSRF::AppSession->create('open-ils.storage'),
1505 $copy, $self->editor->requestor );
1508 $logger->debug("circulator: no potential permitted".
1509 "holds found for copy ".$copy->barcode);
1514 $logger->info("circulator: found permitted hold ".
1515 $hold->id . " for copy, capturing...");
1517 $hold->current_copy($copy->id);
1518 $hold->capture_time('now');
1520 # prevent DB errors caused by fetching
1521 # holds from storage, and updating through cstore
1522 $hold->clear_fulfillment_time;
1523 $hold->clear_fulfillment_staff;
1524 $hold->clear_fulfillment_lib;
1525 $hold->clear_expire_time;
1526 $hold->clear_cancel_time;
1527 $hold->clear_prev_check_time unless $hold->prev_check_time;
1529 $self->bail_on_events($self->editor->event)
1530 unless $self->editor->update_action_hold_request($hold);
1532 $self->checkin_changed(1);
1534 return 1 if $self->bail_out;
1536 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1538 # This hold was captured in the correct location
1539 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1540 $self->push_events(OpenILS::Event->new('SUCCESS'));
1542 #$self->do_hold_notify($hold->id);
1543 $self->notify_hold($hold->id);
1547 # Hold needs to be picked up elsewhere. Build a hold
1548 # transit and route the item.
1549 $self->checkin_build_hold_transit();
1550 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1551 return 1 if $self->bail_out;
1553 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1556 # make sure we save the copy status
1561 sub do_hold_notify {
1562 my( $self, $holdid ) = @_;
1564 $logger->info("circulator: running delayed hold notify process");
1566 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1567 hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1569 $logger->debug("circulator: built hold notifier");
1571 if(!$notifier->event) {
1573 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1575 my $stat = $notifier->send_email_notify;
1576 if( $stat == '1' ) {
1577 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1581 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1584 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1589 sub checkin_build_hold_transit {
1592 my $copy = $self->copy;
1593 my $hold = $self->hold;
1594 my $trans = Fieldmapper::action::hold_transit_copy->new;
1596 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1598 $trans->hold($hold->id);
1599 $trans->source($self->editor->requestor->ws_ou);
1600 $trans->dest($hold->pickup_lib);
1601 $trans->source_send_time("now");
1602 $trans->target_copy($copy->id);
1604 # when the copy gets to its destination, it will recover
1605 # this status - put it onto the holds shelf
1606 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1608 return $self->bail_on_events($self->editor->event)
1609 unless $self->editor->create_action_hold_transit_copy($trans);
1614 sub process_received_transit {
1616 my $copy = $self->copy;
1617 my $copyid = $self->copy->id;
1619 my $status_name = $U->copy_status($copy->status)->name;
1620 $logger->debug("circulator: attempting transit receive on ".
1621 "copy $copyid. Copy status is $status_name");
1623 my $transit = $self->transit;
1625 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1626 $logger->info("circulator: Fowarding transit on copy which is destined ".
1627 "for a different location. copy=$copyid,current ".
1628 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1630 return $self->bail_on_events(
1631 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1634 # The transit is received, set the receive time
1635 $transit->dest_recv_time('now');
1636 $self->bail_on_events($self->editor->event)
1637 unless $self->editor->update_action_transit_copy($transit);
1639 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1641 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1642 $copy->status( $transit->copy_status );
1643 $self->update_copy();
1644 return if $self->bail_out;
1648 #$self->do_hold_notify($hold_transit->hold);
1649 $self->notify_hold($hold_transit->hold);
1654 OpenILS::Event->new(
1657 payload => { transit => $transit, holdtransit => $hold_transit } ));
1659 return $hold_transit;
1663 sub checkin_handle_circ {
1667 my $circ = $self->circ;
1668 my $copy = $self->copy;
1672 # backdate the circ if necessary
1673 if($self->backdate) {
1674 $self->checkin_handle_backdate;
1675 return if $self->bail_out;
1678 if(!$circ->stop_fines) {
1679 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1680 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1681 $circ->stop_fines_time('now') unless $self->backdate;
1682 $circ->stop_fines_time($self->backdate) if $self->backdate;
1685 # see if there are any fines owed on this circ. if not, close it
1686 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1687 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1689 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1691 # Set the checkin vars since we have the item
1692 $circ->checkin_time('now');
1693 $circ->checkin_staff($self->editor->requestor->id);
1694 $circ->checkin_lib($self->editor->requestor->ws_ou);
1696 my $circ_lib = (ref $self->copy->circ_lib) ?
1697 $self->copy->circ_lib->id : $self->copy->circ_lib;
1698 my $stat = $U->copy_status($self->copy->status)->id;
1700 # If the item is lost/missing and it needs to be sent home, don't
1701 # reshelve the copy, leave it lost/missing so the recipient will know
1702 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1703 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1704 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1707 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1712 return $self->bail_on_events($self->editor->event)
1713 unless $self->editor->update_action_circulation($circ);
1717 sub checkin_handle_backdate {
1720 my $bd = $self->backdate;
1721 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1722 $bd = "${bd}T23:59:59";
1724 my $bills = $self->editor->search_money_billing(
1726 billing_ts => { '>=' => $bd },
1727 xact => $self->circ->id,
1728 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1732 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1734 for my $bill (@$bills) {
1735 unless( $U->is_true($bill->voided) ) {
1736 $logger->info("backdate voiding bill ".$bill->id);
1738 $bill->void_time('now');
1739 $bill->voider($self->editor->requestor->id);
1740 my $n = $bill->note || "";
1741 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1743 $self->bail_on_events($self->editor->event)
1744 unless $self->editor->update_money_billing($bill);
1752 # XXX Legacy version for Circ.pm support
1753 sub _checkin_handle_backdate {
1754 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1757 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1758 $bd = "${bd}T23:59:59";
1760 my $bills = $session->request(
1761 "open-ils.storage.direct.money.billing.search_where.atomic",
1762 billing_ts => { '>=' => $bd },
1764 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1767 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1770 for my $bill (@$bills) {
1771 unless( $U->is_true($bill->voided) ) {
1772 $logger->debug("voiding bill ".$bill->id);
1774 $bill->void_time('now');
1775 $bill->voider($requestor->id);
1776 my $n = $bill->note || "";
1777 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1778 my $s = $session->request(
1779 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1780 return $U->DB_UPDATE_FAILED($bill) unless $s;
1794 sub find_patron_from_copy {
1796 my $circs = $self->editor->search_action_circulation(
1797 { target_copy => $self->copy->id, checkin_time => undef });
1798 my $circ = $circs->[0];
1799 return unless $circ;
1800 my $u = $self->editor->retrieve_actor_user($circ->usr)
1801 or return $self->bail_on_events($self->editor->event);
1805 sub check_checkin_copy_status {
1807 my $copy = $self->copy;
1813 my $status = $U->copy_status($copy->status)->id;
1816 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1817 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1818 $status == OILS_COPY_STATUS_IN_PROCESS ||
1819 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1820 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1821 $status == OILS_COPY_STATUS_CATALOGING ||
1822 $status == OILS_COPY_STATUS_RESHELVING );
1824 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1825 if( $status == OILS_COPY_STATUS_LOST );
1827 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1828 if( $status == OILS_COPY_STATUS_MISSING );
1830 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1835 # --------------------------------------------------------------------------
1836 # On checkin, we need to return as many relevant objects as we can
1837 # --------------------------------------------------------------------------
1838 sub checkin_flesh_events {
1841 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1842 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1843 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1847 for my $evt (@{$self->events}) {
1850 $payload->{copy} = $U->unflesh_copy($self->copy);
1851 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1852 $payload->{circ} = $self->circ;
1853 $payload->{transit} = $self->transit;
1854 $payload->{hold} = $self->hold;
1856 $evt->{payload} = $payload;
1861 my( $self, $msg ) = @_;
1862 my $bc = ($self->copy) ? $self->copy->barcode :
1865 my $usr = ($self->patron) ? $self->patron->id : "";
1866 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1867 ", recipient=$usr, copy=$bc");
1873 $self->log_me("do_renew()");
1874 $self->is_renewal(1);
1876 unless( $self->is_renewal ) {
1877 return $self->bail_on_events($self->editor->events)
1878 unless $self->editor->allowed('RENEW_CIRC');
1881 # Make sure there is an open circ to renew that is not
1882 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1883 my $circ = $self->editor->search_action_circulation(
1884 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1887 $circ = $self->editor->search_action_circulation(
1889 target_copy => $self->copy->id,
1890 stop_fines => OILS_STOP_FINES_MAX_FINES,
1891 checkin_time => undef
1896 return $self->bail_on_events($self->editor->event) unless $circ;
1898 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1899 if $circ->renewal_remaining < 1;
1901 # -----------------------------------------------------------------
1903 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1906 $self->run_renew_permit;
1909 $self->do_checkin();
1910 return if $self->bail_out;
1912 unless( $self->permit_override ) {
1914 return if $self->bail_out;
1915 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1916 $self->remove_event('ITEM_NOT_CATALOGED');
1919 $self->override_events;
1920 return if $self->bail_out;
1923 $self->do_checkout();
1928 my( $self, $evt ) = @_;
1929 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1930 $logger->debug("circulator: removing event from list: $evt");
1931 my @events = @{$self->events};
1932 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1937 my( $self, $evt ) = @_;
1938 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1939 return grep { $_->{textcode} eq $evt } @{$self->events};
1944 sub run_renew_permit {
1946 my $runner = $self->script_runner;
1948 $runner->load($self->circ_permit_renew);
1949 my $result = $runner->run or
1950 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1951 my $events = $result->{events};
1953 $logger->activity("ciculator: circ_permit_renew for user ".
1954 $self->patron->id." returned events: @$events") if @$events;
1956 $self->push_events(OpenILS::Event->new($_)) for @$events;
1958 $logger->debug("circulator: re-creating script runner to be safe");
1959 $self->mk_script_runner;