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 # due-date overlap should be determined by the location the item
1128 # is checked out from, not the owning or circ lib of the item
1129 my $org = $self->editor->requestor->ws_ou;
1131 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1132 " with an item due date of ".$circ->due_date );
1134 my $dateinfo = $U->storagereq(
1135 'open-ils.storage.actor.org_unit.closed_date.overlap',
1136 $org, $circ->due_date );
1139 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1140 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1142 # XXX make the behavior more dynamic
1143 # for now, we just push the due date to after the close date
1144 $circ->due_date($dateinfo->{end});
1151 sub create_due_date {
1152 my( $self, $duration ) = @_;
1153 my ($sec,$min,$hour,$mday,$mon,$year) =
1154 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1155 $year += 1900; $mon += 1;
1156 my $due_date = sprintf(
1157 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1158 $year, $mon, $mday, $hour, $min, $sec);
1164 sub make_precat_copy {
1166 my $copy = $self->copy;
1169 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1171 $copy->editor($self->editor->requestor->id);
1172 $copy->edit_date('now');
1173 $copy->dummy_title($self->dummy_title);
1174 $copy->dummy_author($self->dummy_author);
1176 $self->update_copy();
1180 $logger->info("circulator: Creating a new precataloged ".
1181 "copy in checkout with barcode " . $self->copy_barcode);
1183 $copy = Fieldmapper::asset::copy->new;
1184 $copy->circ_lib($self->circ_lib);
1185 $copy->creator($self->editor->requestor->id);
1186 $copy->editor($self->editor->requestor->id);
1187 $copy->barcode($self->copy_barcode);
1188 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1189 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1190 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1192 $copy->dummy_title($self->dummy_title || "");
1193 $copy->dummy_author($self->dummy_author || "");
1195 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1197 $self->push_events($self->editor->event);
1201 # this is a little bit of a hack, but we need to
1202 # get the copy into the script runner
1203 $self->script_runner->insert("environment.copy", $copy, 1);
1207 sub checkout_noncat {
1213 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1214 my $count = $self->noncat_count || 1;
1215 my $cotime = clense_ISO8601($self->checkout_time) || "";
1217 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1221 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1222 $self->editor->requestor->id,
1230 $self->push_events($evt);
1241 $self->log_me("do_checkin()");
1244 return $self->bail_on_events(
1245 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1248 if( $self->checkin_check_holds_shelf() ) {
1249 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1250 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1251 $self->checkin_flesh_events;
1255 unless( $self->is_renewal ) {
1256 return $self->bail_on_events($self->editor->event)
1257 unless $self->editor->allowed('COPY_CHECKIN');
1260 $self->push_events($self->check_copy_alert());
1261 $self->push_events($self->check_checkin_copy_status());
1263 # the renew code will have already found our circulation object
1264 unless( $self->is_renewal and $self->circ ) {
1266 $self->editor->search_action_circulation(
1267 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1270 # if the circ is marked as 'claims returned', add the event to the list
1271 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1272 if ($self->circ and $self->circ->stop_fines
1273 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1275 # handle the overridable events
1276 $self->override_events unless $self->is_renewal;
1277 return if $self->bail_out;
1281 $self->editor->search_action_transit_copy(
1282 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1286 $self->checkin_handle_circ;
1287 return if $self->bail_out;
1288 $self->checkin_changed(1);
1290 } elsif( $self->transit ) {
1291 my $hold_transit = $self->process_received_transit;
1292 $self->checkin_changed(1);
1294 if( $self->bail_out ) {
1295 $self->checkin_flesh_events;
1299 if( my $e = $self->check_checkin_copy_status() ) {
1300 # If the original copy status is special, alert the caller
1301 my $ev = $self->events;
1302 $self->events([$e]);
1303 $self->override_events;
1304 return if $self->bail_out;
1308 if( $hold_transit or
1309 $U->copy_status($self->copy->status)->id
1310 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1313 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1314 $U->fetch_open_hold_by_copy($self->copy->id)
1317 $self->checkin_flesh_events;
1321 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1322 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1323 " that is in-transit, but there is no transit.. repairing");
1324 $self->reshelve_copy(1);
1325 return if $self->bail_out;
1328 if( $self->is_renewal ) {
1329 $self->push_events(OpenILS::Event->new('SUCCESS'));
1333 # ------------------------------------------------------------------------------
1334 # Circulations and transits are now closed where necessary. Now go on to see if
1335 # this copy can fulfill a hold or needs to be routed to a different location
1336 # ------------------------------------------------------------------------------
1338 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1339 return if $self->bail_out;
1341 } else { # not needed for a hold
1344 my $circ_lib = (ref $self->copy->circ_lib) ?
1345 $self->copy->circ_lib->id : $self->copy->circ_lib;
1347 if( $self->remote_hold ) {
1348 $circ_lib = $self->remote_hold->pickup_lib;
1349 $logger->warn("circulator: Copy ".$self->copy->barcode.
1350 " is on a remote hold's shelf, sending to $circ_lib");
1353 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1355 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1357 $self->checkin_handle_precat();
1358 return if $self->bail_out;
1362 my $bc = $self->copy->barcode;
1363 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1364 $self->checkin_build_copy_transit($circ_lib);
1365 return if $self->bail_out;
1366 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1370 $self->reshelve_copy;
1371 return if $self->bail_out;
1373 unless($self->checkin_changed) {
1375 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1376 my $stat = $U->copy_status($self->copy->status)->id;
1378 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1379 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1380 $self->bail_out(1); # no need to commit anything
1383 $self->push_events(OpenILS::Event->new('SUCCESS'))
1384 unless @{$self->events};
1388 # ------------------------------------------------------------------------------
1389 # Update the patron penalty info in the DB
1390 # ------------------------------------------------------------------------------
1391 $U->update_patron_penalties(
1392 authtoken => $self->editor->authtoken,
1393 patron => $self->patron,
1394 background => 1 ) if $self->is_checkin;
1396 $self->checkin_flesh_events;
1402 my $force = $self->force || shift;
1403 my $copy = $self->copy;
1405 my $stat = $U->copy_status($copy->status)->id;
1408 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1409 $stat != OILS_COPY_STATUS_CATALOGING and
1410 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1411 $stat != OILS_COPY_STATUS_RESHELVING )) {
1413 $copy->status( OILS_COPY_STATUS_RESHELVING );
1415 $self->checkin_changed(1);
1420 # Returns true if the item is at the current location
1421 # because it was transited there for a hold and the
1422 # hold has not been fulfilled
1423 sub checkin_check_holds_shelf {
1425 return 0 unless $self->copy;
1428 $U->copy_status($self->copy->status)->id ==
1429 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1431 # find the hold that put us on the holds shelf
1432 my $holds = $self->editor->search_action_hold_request(
1434 current_copy => $self->copy->id,
1435 capture_time => { '!=' => undef },
1436 fulfillment_time => undef,
1437 cancel_time => undef,
1442 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1443 $self->reshelve_copy(1);
1447 my $hold = $$holds[0];
1449 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1450 $hold->id. "] for copy ".$self->copy->barcode);
1452 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1453 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1457 $logger->info("circulator: hold is not for here..");
1458 $self->remote_hold($hold);
1463 sub checkin_handle_precat {
1465 my $copy = $self->copy;
1467 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1468 $copy->status(OILS_COPY_STATUS_CATALOGING);
1469 $self->update_copy();
1470 $self->checkin_changed(1);
1471 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1476 sub checkin_build_copy_transit {
1479 my $copy = $self->copy;
1480 my $transit = Fieldmapper::action::transit_copy->new;
1482 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1483 $logger->info("circulator: transiting copy to $dest");
1485 $transit->source($self->editor->requestor->ws_ou);
1486 $transit->dest($dest);
1487 $transit->target_copy($copy->id);
1488 $transit->source_send_time('now');
1489 $transit->copy_status( $U->copy_status($copy->status)->id );
1491 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1493 return $self->bail_on_events($self->editor->event)
1494 unless $self->editor->create_action_transit_copy($transit);
1496 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1498 $self->checkin_changed(1);
1502 sub attempt_checkin_hold_capture {
1504 my $copy = $self->copy;
1506 # See if this copy can fulfill any holds
1507 my ($hold) = $holdcode->find_nearest_permitted_hold(
1508 OpenSRF::AppSession->create('open-ils.storage'),
1509 $copy, $self->editor->requestor );
1512 $logger->debug("circulator: no potential permitted".
1513 "holds found for copy ".$copy->barcode);
1518 $logger->info("circulator: found permitted hold ".
1519 $hold->id . " for copy, capturing...");
1521 $hold->current_copy($copy->id);
1522 $hold->capture_time('now');
1524 # prevent DB errors caused by fetching
1525 # holds from storage, and updating through cstore
1526 $hold->clear_fulfillment_time;
1527 $hold->clear_fulfillment_staff;
1528 $hold->clear_fulfillment_lib;
1529 $hold->clear_expire_time;
1530 $hold->clear_cancel_time;
1531 $hold->clear_prev_check_time unless $hold->prev_check_time;
1533 $self->bail_on_events($self->editor->event)
1534 unless $self->editor->update_action_hold_request($hold);
1536 $self->checkin_changed(1);
1538 return 1 if $self->bail_out;
1540 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1542 # This hold was captured in the correct location
1543 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1544 $self->push_events(OpenILS::Event->new('SUCCESS'));
1546 #$self->do_hold_notify($hold->id);
1547 $self->notify_hold($hold->id);
1551 # Hold needs to be picked up elsewhere. Build a hold
1552 # transit and route the item.
1553 $self->checkin_build_hold_transit();
1554 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1555 return 1 if $self->bail_out;
1557 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1560 # make sure we save the copy status
1565 sub do_hold_notify {
1566 my( $self, $holdid ) = @_;
1568 $logger->info("circulator: running delayed hold notify process");
1570 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1571 hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1573 $logger->debug("circulator: built hold notifier");
1575 if(!$notifier->event) {
1577 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1579 my $stat = $notifier->send_email_notify;
1580 if( $stat == '1' ) {
1581 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1585 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1588 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1593 sub checkin_build_hold_transit {
1596 my $copy = $self->copy;
1597 my $hold = $self->hold;
1598 my $trans = Fieldmapper::action::hold_transit_copy->new;
1600 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1602 $trans->hold($hold->id);
1603 $trans->source($self->editor->requestor->ws_ou);
1604 $trans->dest($hold->pickup_lib);
1605 $trans->source_send_time("now");
1606 $trans->target_copy($copy->id);
1608 # when the copy gets to its destination, it will recover
1609 # this status - put it onto the holds shelf
1610 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1612 return $self->bail_on_events($self->editor->event)
1613 unless $self->editor->create_action_hold_transit_copy($trans);
1618 sub process_received_transit {
1620 my $copy = $self->copy;
1621 my $copyid = $self->copy->id;
1623 my $status_name = $U->copy_status($copy->status)->name;
1624 $logger->debug("circulator: attempting transit receive on ".
1625 "copy $copyid. Copy status is $status_name");
1627 my $transit = $self->transit;
1629 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1630 my $tid = $transit->id;
1631 $logger->info("circulator: Fowarding transit on copy which is destined ".
1632 "for a different location. transit=$tid, copy=$copyid,current ".
1633 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1635 return $self->bail_on_events(
1636 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1639 # The transit is received, set the receive time
1640 $transit->dest_recv_time('now');
1641 $self->bail_on_events($self->editor->event)
1642 unless $self->editor->update_action_transit_copy($transit);
1644 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1646 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1647 $copy->status( $transit->copy_status );
1648 $self->update_copy();
1649 return if $self->bail_out;
1653 #$self->do_hold_notify($hold_transit->hold);
1654 $self->notify_hold($hold_transit->hold);
1659 OpenILS::Event->new(
1662 payload => { transit => $transit, holdtransit => $hold_transit } ));
1664 return $hold_transit;
1668 sub checkin_handle_circ {
1672 my $circ = $self->circ;
1673 my $copy = $self->copy;
1677 # backdate the circ if necessary
1678 if($self->backdate) {
1679 $self->checkin_handle_backdate;
1680 return if $self->bail_out;
1683 if(!$circ->stop_fines) {
1684 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1685 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1686 $circ->stop_fines_time('now') unless $self->backdate;
1687 $circ->stop_fines_time($self->backdate) if $self->backdate;
1690 # see if there are any fines owed on this circ. if not, close it
1691 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1692 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1694 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1696 # Set the checkin vars since we have the item
1697 $circ->checkin_time('now');
1698 $circ->checkin_staff($self->editor->requestor->id);
1699 $circ->checkin_lib($self->editor->requestor->ws_ou);
1701 my $circ_lib = (ref $self->copy->circ_lib) ?
1702 $self->copy->circ_lib->id : $self->copy->circ_lib;
1703 my $stat = $U->copy_status($self->copy->status)->id;
1705 # If the item is lost/missing and it needs to be sent home, don't
1706 # reshelve the copy, leave it lost/missing so the recipient will know
1707 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1708 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1709 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1712 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1717 return $self->bail_on_events($self->editor->event)
1718 unless $self->editor->update_action_circulation($circ);
1722 sub checkin_handle_backdate {
1725 my $bd = $self->backdate;
1726 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1727 $bd = "${bd}T23:59:59";
1729 my $bills = $self->editor->search_money_billing(
1731 billing_ts => { '>=' => $bd },
1732 xact => $self->circ->id,
1733 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1737 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1739 for my $bill (@$bills) {
1740 unless( $U->is_true($bill->voided) ) {
1741 $logger->info("backdate voiding bill ".$bill->id);
1743 $bill->void_time('now');
1744 $bill->voider($self->editor->requestor->id);
1745 my $n = $bill->note || "";
1746 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1748 $self->bail_on_events($self->editor->event)
1749 unless $self->editor->update_money_billing($bill);
1757 # XXX Legacy version for Circ.pm support
1758 sub _checkin_handle_backdate {
1759 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1762 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1763 $bd = "${bd}T23:59:59";
1765 my $bills = $session->request(
1766 "open-ils.storage.direct.money.billing.search_where.atomic",
1767 billing_ts => { '>=' => $bd },
1769 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1772 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1775 for my $bill (@$bills) {
1776 unless( $U->is_true($bill->voided) ) {
1777 $logger->debug("voiding bill ".$bill->id);
1779 $bill->void_time('now');
1780 $bill->voider($requestor->id);
1781 my $n = $bill->note || "";
1782 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1783 my $s = $session->request(
1784 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1785 return $U->DB_UPDATE_FAILED($bill) unless $s;
1799 sub find_patron_from_copy {
1801 my $circs = $self->editor->search_action_circulation(
1802 { target_copy => $self->copy->id, checkin_time => undef });
1803 my $circ = $circs->[0];
1804 return unless $circ;
1805 my $u = $self->editor->retrieve_actor_user($circ->usr)
1806 or return $self->bail_on_events($self->editor->event);
1810 sub check_checkin_copy_status {
1812 my $copy = $self->copy;
1818 my $status = $U->copy_status($copy->status)->id;
1821 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1822 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1823 $status == OILS_COPY_STATUS_IN_PROCESS ||
1824 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1825 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1826 $status == OILS_COPY_STATUS_CATALOGING ||
1827 $status == OILS_COPY_STATUS_RESHELVING );
1829 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1830 if( $status == OILS_COPY_STATUS_LOST );
1832 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1833 if( $status == OILS_COPY_STATUS_MISSING );
1835 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1840 # --------------------------------------------------------------------------
1841 # On checkin, we need to return as many relevant objects as we can
1842 # --------------------------------------------------------------------------
1843 sub checkin_flesh_events {
1846 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1847 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1848 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1852 for my $evt (@{$self->events}) {
1855 $payload->{copy} = $U->unflesh_copy($self->copy);
1856 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1857 $payload->{circ} = $self->circ;
1858 $payload->{transit} = $self->transit;
1859 $payload->{hold} = $self->hold;
1861 $evt->{payload} = $payload;
1866 my( $self, $msg ) = @_;
1867 my $bc = ($self->copy) ? $self->copy->barcode :
1870 my $usr = ($self->patron) ? $self->patron->id : "";
1871 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1872 ", recipient=$usr, copy=$bc");
1878 $self->log_me("do_renew()");
1879 $self->is_renewal(1);
1881 unless( $self->is_renewal ) {
1882 return $self->bail_on_events($self->editor->events)
1883 unless $self->editor->allowed('RENEW_CIRC');
1886 # Make sure there is an open circ to renew that is not
1887 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1888 my $circ = $self->editor->search_action_circulation(
1889 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1892 $circ = $self->editor->search_action_circulation(
1894 target_copy => $self->copy->id,
1895 stop_fines => OILS_STOP_FINES_MAX_FINES,
1896 checkin_time => undef
1901 return $self->bail_on_events($self->editor->event) unless $circ;
1903 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1904 if $circ->renewal_remaining < 1;
1906 # -----------------------------------------------------------------
1908 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1911 $self->run_renew_permit;
1914 $self->do_checkin();
1915 return if $self->bail_out;
1917 unless( $self->permit_override ) {
1919 return if $self->bail_out;
1920 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1921 $self->remove_event('ITEM_NOT_CATALOGED');
1924 $self->override_events;
1925 return if $self->bail_out;
1928 $self->do_checkout();
1933 my( $self, $evt ) = @_;
1934 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1935 $logger->debug("circulator: removing event from list: $evt");
1936 my @events = @{$self->events};
1937 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1942 my( $self, $evt ) = @_;
1943 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1944 return grep { $_->{textcode} eq $evt } @{$self->events};
1949 sub run_renew_permit {
1951 my $runner = $self->script_runner;
1953 $runner->load($self->circ_permit_renew);
1954 my $result = $runner->run or
1955 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1956 my $events = $result->{events};
1958 $logger->activity("ciculator: circ_permit_renew for user ".
1959 $self->patron->id." returned events: @$events") if @$events;
1961 $self->push_events(OpenILS::Event->new($_)) for @$events;
1963 $logger->debug("circulator: re-creating script runner to be safe");
1964 $self->mk_script_runner;