1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenSRF::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
15 my $conf = OpenSRF::Utils::SettingsClient->new;
16 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
17 my @pfx = ( @pfx2, "scripts" );
19 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
20 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
21 my $d = $conf->config_value( @pfx, 'circ_duration' );
22 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
23 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
24 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
25 my $lb = $conf->config_value( @pfx2, 'script_path' );
27 $logger->error( "Missing circ script(s)" )
28 unless( $p and $c and $d and $f and $m and $pr );
30 $scripts{circ_permit_patron} = $p;
31 $scripts{circ_permit_copy} = $c;
32 $scripts{circ_duration} = $d;
33 $scripts{circ_recurring_fines}= $f;
34 $scripts{circ_max_fines} = $m;
35 $scripts{circ_permit_renew} = $pr;
37 $lb = [ $lb ] unless ref($lb);
41 "circulator: Loaded rules scripts for circ: " .
42 "circ permit patron = $p, ".
43 "circ permit copy = $c, ".
44 "circ duration = $d, ".
45 "circ recurring fines = $f, " .
46 "circ max fines = $m, ".
47 "circ renew permit = $pr. ".
52 __PACKAGE__->register_method(
53 method => "run_method",
54 api_name => "open-ils.circ.checkout.permit",
56 Determines if the given checkout can occur
57 @param authtoken The login session key
58 @param params A trailing hash of named params including
59 barcode : The copy barcode,
60 patron : The patron the checkout is occurring for,
61 renew : true or false - whether or not this is a renewal
62 @return The event that occurred during the permit check.
66 __PACKAGE__->register_method (
67 method => 'run_method',
68 api_name => 'open-ils.circ.checkout.permit.override',
69 signature => q/@see open-ils.circ.checkout.permit/,
73 __PACKAGE__->register_method(
74 method => "run_method",
75 api_name => "open-ils.circ.checkout",
78 @param authtoken The login session key
79 @param params A named hash of params including:
81 barcode If no copy is provided, the copy is retrieved via barcode
82 copyid If no copy or barcode is provide, the copy id will be use
83 patron The patron's id
84 noncat True if this is a circulation for a non-cataloted item
85 noncat_type The non-cataloged type id
86 noncat_circ_lib The location for the noncat circ.
87 precat The item has yet to be cataloged
88 dummy_title The temporary title of the pre-cataloded item
89 dummy_author The temporary authr of the pre-cataloded item
90 Default is the home org of the staff member
91 @return The SUCCESS event on success, any other event depending on the error
94 __PACKAGE__->register_method(
95 method => "run_method",
96 api_name => "open-ils.circ.checkin",
99 Generic super-method for handling all copies
100 @param authtoken The login session key
101 @param params Hash of named parameters including:
102 barcode - The copy barcode
103 force - If true, copies in bad statuses will be checked in and give good statuses
108 __PACKAGE__->register_method(
109 method => "run_method",
110 api_name => "open-ils.circ.checkin.override",
111 signature => q/@see open-ils.circ.checkin/
114 __PACKAGE__->register_method(
115 method => "run_method",
116 api_name => "open-ils.circ.renew.override",
117 signature => q/@see open-ils.circ.renew/,
121 __PACKAGE__->register_method(
122 method => "run_method",
123 api_name => "open-ils.circ.renew",
124 notes => <<" NOTES");
125 PARAMS( authtoken, circ => circ_id );
126 open-ils.circ.renew(login_session, circ_object);
127 Renews the provided circulation. login_session is the requestor of the
128 renewal and if the logged in user is not the same as circ->usr, then
129 the logged in user must have RENEW_CIRC permissions.
132 __PACKAGE__->register_method(
133 method => "run_method",
134 api_name => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136 method => "run_method",
137 api_name => "open-ils.circ.checkout.full.override");
142 my( $self, $conn, $auth, $args ) = @_;
143 translate_legacy_args($args);
144 my $api = $self->api_name;
147 OpenILS::Application::Circ::Circulator->new($auth, %$args);
149 return circ_events($circulator) if $circulator->bail_out;
151 # --------------------------------------------------------------------------
152 # Go ahead and load the script runner to make sure we have all
153 # of the objects we need
154 # --------------------------------------------------------------------------
155 $circulator->is_renewal(1) if $api =~ /renew/;
156 $circulator->is_checkin(1) if $api =~ /checkin/;
157 $circulator->mk_script_runner;
158 return circ_events($circulator) if $circulator->bail_out;
160 $circulator->circ_permit_patron($scripts{circ_permit_patron});
161 $circulator->circ_permit_copy($scripts{circ_permit_copy});
162 $circulator->circ_duration($scripts{circ_duration});
163 $circulator->circ_permit_renew($scripts{circ_permit_renew});
165 $circulator->override(1) if $api =~ /override/o;
167 if( $api =~ /checkout\.permit/ ) {
168 $circulator->do_permit();
170 } elsif( $api =~ /checkout.full/ ) {
172 $circulator->do_permit();
173 unless( $circulator->bail_out ) {
174 $circulator->events([]);
175 $circulator->do_checkout();
178 } elsif( $api =~ /checkout/ ) {
179 $circulator->do_checkout();
181 } elsif( $api =~ /checkin/ ) {
182 $circulator->do_checkin();
184 } elsif( $api =~ /renew/ ) {
185 $circulator->is_renewal(1);
186 $circulator->do_renew();
189 if( $circulator->bail_out ) {
192 # make sure no success event accidentally slip in
194 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
195 my @e = @{$circulator->events};
196 push( @ee, $_->{textcode} ) for @e;
197 $logger->info("circulator: bailing out with events: @ee");
198 $circulator->editor->rollback;
201 $circulator->editor->commit;
204 $circulator->script_runner->cleanup;
206 $conn->respond_complete(circ_events($circulator));
208 unless($circulator->bail_out) {
209 $logger->info("circulator: running delayed hold notify process");
210 $circulator->do_hold_notify($circulator->notify_hold)
211 if $circulator->notify_hold;
217 my @e = @{$circ->events};
218 return (@e == 1) ? $e[0] : \@e;
223 sub translate_legacy_args {
226 if( $$args{barcode} ) {
227 $$args{copy_barcode} = $$args{barcode};
228 delete $$args{barcode};
231 if( $$args{copyid} ) {
232 $$args{copy_id} = $$args{copyid};
233 delete $$args{copyid};
236 if( $$args{patronid} ) {
237 $$args{patron_id} = $$args{patronid};
238 delete $$args{patronid};
241 if( $$args{patron} and !ref($$args{patron}) ) {
242 $$args{patron_id} = $$args{patron};
243 delete $$args{patron};
247 if( $$args{noncat} ) {
248 $$args{is_noncat} = $$args{noncat};
249 delete $$args{noncat};
252 if( $$args{precat} ) {
253 $$args{is_precat} = $$args{precat};
254 delete $$args{precat};
260 # --------------------------------------------------------------------------
261 # This package actually manages all of the circulation logic
262 # --------------------------------------------------------------------------
263 package OpenILS::Application::Circ::Circulator;
264 use strict; use warnings;
265 use vars q/$AUTOLOAD/;
267 use OpenILS::Utils::Fieldmapper;
268 use OpenSRF::Utils::Cache;
269 use Digest::MD5 qw(md5_hex);
270 use DateTime::Format::ISO8601;
271 use OpenILS::Utils::PermitHold;
272 use OpenSRF::Utils qw/:datetime/;
273 use OpenSRF::Utils::SettingsClient;
274 use OpenILS::Application::Circ::Holds;
275 use OpenILS::Application::Circ::Transit;
276 use OpenSRF::Utils::Logger qw(:logger);
277 use OpenILS::Utils::CStoreEditor qw/:funcs/;
278 use OpenILS::Application::Circ::ScriptBuilder;
279 use OpenILS::Const qw/:const/;
281 my $U = "OpenILS::Application::AppUtils";
282 my $holdcode = "OpenILS::Application::Circ::Holds";
283 my $transcode = "OpenILS::Application::Circ::Transit";
288 # --------------------------------------------------------------------------
289 # Add a pile of automagic getter/setter methods
290 # --------------------------------------------------------------------------
291 my @AUTOLOAD_FIELDS = qw/
332 recurring_fines_level
349 my $type = ref($self) or die "$self is not an object";
351 my $name = $AUTOLOAD;
354 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
355 $logger->error("circulator: $type: invalid autoload field: $name");
356 die "$type: invalid autoload field: $name\n"
361 *{"${type}::${name}"} = sub {
364 $s->{$name} = $v if defined $v;
368 return $self->$name($data);
373 my( $class, $auth, %args ) = @_;
374 $class = ref($class) || $class;
375 my $self = bless( {}, $class );
379 new_editor(xact => 1, authtoken => $auth) );
381 unless( $self->editor->checkauth ) {
382 $self->bail_on_events($self->editor->event);
386 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
388 $self->$_($args{$_}) for keys %args;
391 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
397 # --------------------------------------------------------------------------
398 # True if we should discontinue processing
399 # --------------------------------------------------------------------------
401 my( $self, $bool ) = @_;
402 if( defined $bool ) {
403 $logger->info("circulator: BAILING OUT") if $bool;
404 $self->{bail_out} = $bool;
406 return $self->{bail_out};
411 my( $self, @evts ) = @_;
414 $logger->info("circulator: pushing event ".$e->{textcode});
415 push( @{$self->events}, $e ) unless
416 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
422 my $key = md5_hex( time() . rand() . "$$" );
423 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
424 return $self->permit_key($key);
427 sub check_permit_key {
429 my $key = $self->permit_key;
430 return 0 unless $key;
431 my $k = "oils_permit_key_$key";
432 my $one = $self->cache_handle->get_cache($k);
433 $self->cache_handle->delete_cache($k);
434 return ($one) ? 1 : 0;
438 # --------------------------------------------------------------------------
439 # This builds the script runner environment and fetches most of the
441 # --------------------------------------------------------------------------
442 sub mk_script_runner {
448 qw/copy copy_barcode copy_id patron
449 patron_id patron_barcode volume title editor/;
451 # Translate our objects into the ScriptBuilder args hash
452 $$args{$_} = $self->$_() for @fields;
454 $args->{ignore_user_status} = 1 if $self->is_checkin;
455 $$args{fetch_patron_by_circ_copy} = 1;
456 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
458 # This fetches most of the objects we need
459 $self->script_runner(
460 OpenILS::Application::Circ::ScriptBuilder->build($args));
462 # Now we translate the ScriptBuilder objects back into self
463 $self->$_($$args{$_}) for @fields;
465 my @evts = @{$args->{_events}} if $args->{_events};
467 $logger->debug("circulator: script builder returned events: @evts") if @evts;
471 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
472 if(!$self->is_noncat and
474 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
478 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
479 return $self->bail_on_events(@e);
483 $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
485 # We can't renew if there is no copy
486 return $self->bail_on_events(@evts) if
487 $self->is_renewal and !$self->copy;
489 # Set some circ-specific flags in the script environment
490 my $evt = "environment";
491 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
493 if( $self->is_noncat ) {
494 $self->script_runner->insert("$evt.isNonCat", 1);
495 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
498 if( $self->is_precat ) {
499 $self->script_runner->insert("environment.isPrecat", 1, 1);
502 $self->script_runner->add_path( $_ ) for @$script_libs;
510 # --------------------------------------------------------------------------
511 # Does the circ permit work
512 # --------------------------------------------------------------------------
516 $self->log_me("do_permit()");
518 unless( $self->editor->requestor->id == $self->patron->id ) {
519 return $self->bail_on_events($self->editor->event)
520 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
524 $self->check_captured_holds();
525 $self->do_copy_checks();
526 return if $self->bail_out;
527 $self->run_patron_permit_scripts();
528 $self->run_copy_permit_scripts()
529 unless $self->is_precat or $self->is_noncat;
530 $self->override_events() unless $self->is_renewal;
531 return if $self->bail_out;
533 if( $self->is_precat ) {
536 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
537 return $self->bail_out(1) unless $self->is_renewal;
543 payload => $self->mk_permit_key));
547 sub check_captured_holds {
549 my $copy = $self->copy;
550 my $patron = $self->patron;
552 return undef unless $copy;
554 my $s = $U->copy_status($copy->status)->id;
555 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
556 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
558 # Item is on the holds shelf, make sure it's going to the right person
559 my $holds = $self->editor->search_action_hold_request(
562 current_copy => $copy->id ,
563 capture_time => { '!=' => undef },
564 cancel_time => undef,
565 fulfillment_time => undef
571 if( $holds and $$holds[0] ) {
572 return undef if $$holds[0]->usr == $patron->id;
575 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
577 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
583 my $copy = $self->copy;
586 my $stat = $U->copy_status($copy->status)->id;
588 # We cannot check out a copy if it is in-transit
589 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
590 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
593 $self->handle_claims_returned();
594 return if $self->bail_out;
596 # no claims returned circ was found, check if there is any open circ
597 unless( $self->is_renewal ) {
598 my $circs = $self->editor->search_action_circulation(
599 { target_copy => $copy->id, checkin_time => undef }
602 return $self->bail_on_events(
603 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
608 sub send_penalty_request {
610 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
611 $self->penalty_request(
613 'open-ils.penalty.patron_penalty.calculate',
615 authtoken => $self->editor->authtoken,
616 patron => $self->patron } ) );
619 sub gather_penalty_request {
621 return [] unless $self->penalty_request;
622 my $data = $self->penalty_request->recv;
624 $data = $data->content;
625 return $data->{fatal_penalties};
627 $logger->error("circulator: penalty request returned no data");
631 # ---------------------------------------------------------------------
632 # This pushes any patron-related events into the list but does not
633 # set bail_out for any events
634 # ---------------------------------------------------------------------
635 sub run_patron_permit_scripts {
637 my $runner = $self->script_runner;
638 my $patronid = $self->patron->id;
640 $self->send_penalty_request();
642 # ---------------------------------------------------------------------
643 # Now run the patron permit script
644 # ---------------------------------------------------------------------
645 $runner->load($self->circ_permit_patron);
646 my $result = $runner->run or
647 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
649 my $patron_events = $result->{events};
652 my $penalties = $self->gather_penalty_request();
654 for my $p (@$penalties, @$patron_events) {
656 # this is policy directly in the code, not a good idea in general, but
657 # the penalty server doesn't know anything about renewals, so we
658 # have to strip the event out here
659 next if $self->is_renewal and $p eq 'PATRON_EXCEEDS_OVERDUE_COUNT';
662 push( @allevents, OpenILS::Event->new($p))
665 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
667 $self->push_events(@allevents);
671 sub run_copy_permit_scripts {
673 my $copy = $self->copy || return;
674 my $runner = $self->script_runner;
676 # ---------------------------------------------------------------------
677 # Capture all of the copy permit events
678 # ---------------------------------------------------------------------
679 $runner->load($self->circ_permit_copy);
680 my $result = $runner->run or
681 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
682 my $copy_events = $result->{events};
684 # ---------------------------------------------------------------------
685 # Now collect all of the events together
686 # ---------------------------------------------------------------------
688 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
690 # See if this copy has an alert message
691 my $ae = $self->check_copy_alert();
692 push( @allevents, $ae ) if $ae;
694 # uniquify the events
695 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
696 @allevents = values %hash;
699 $_->{payload} = $copy if
700 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
703 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
705 $self->push_events(@allevents);
709 sub check_copy_alert {
711 return undef if $self->is_renewal;
712 return OpenILS::Event->new(
713 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
714 if $self->copy and $self->copy->alert_message;
720 # --------------------------------------------------------------------------
721 # If the call is overriding and has permissions to override every collected
722 # event, the are cleared. Any event that the caller does not have
723 # permission to override, will be left in the event list and bail_out will
725 # XXX We need code in here to cancel any holds/transits on copies
726 # that are being force-checked out
727 # --------------------------------------------------------------------------
728 sub override_events {
730 my @events = @{$self->events};
731 return unless @events;
733 if(!$self->override) {
734 return $self->bail_out(1)
735 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
740 for my $e (@events) {
741 my $tc = $e->{textcode};
742 next if $tc eq 'SUCCESS';
743 my $ov = "$tc.override";
744 $logger->info("circulator: attempting to override event: $ov");
746 return $self->bail_on_events($self->editor->event)
747 unless( $self->editor->allowed($ov) );
752 # --------------------------------------------------------------------------
753 # If there is an open claimsreturn circ on the requested copy, close the
754 # circ if overriding, otherwise bail out
755 # --------------------------------------------------------------------------
756 sub handle_claims_returned {
758 my $copy = $self->copy;
760 my $CR = $self->editor->search_action_circulation(
762 target_copy => $copy->id,
763 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
764 checkin_time => undef,
768 return unless ($CR = $CR->[0]);
772 # - If the caller has set the override flag, we will check the item in
773 if($self->override) {
775 $CR->checkin_time('now');
776 $CR->checkin_lib($self->editor->requestor->ws_ou);
777 $CR->checkin_staff($self->editor->requestor->id);
779 $evt = $self->editor->event
780 unless $self->editor->update_action_circulation($CR);
783 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
786 $self->bail_on_events($evt) if $evt;
791 # --------------------------------------------------------------------------
792 # This performs the checkout
793 # --------------------------------------------------------------------------
797 $self->log_me("do_checkout()");
799 # make sure perms are good if this isn't a renewal
800 unless( $self->is_renewal ) {
801 return $self->bail_on_events($self->editor->event)
802 unless( $self->editor->allowed('COPY_CHECKOUT') );
805 # verify the permit key
806 unless( $self->check_permit_key ) {
807 if( $self->permit_override ) {
808 return $self->bail_on_events($self->editor->event)
809 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
811 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
815 # if this is a non-cataloged circ, build the circ and finish
816 if( $self->is_noncat ) {
817 $self->checkout_noncat;
819 OpenILS::Event->new('SUCCESS',
820 payload => { noncat_circ => $self->circ }));
824 if( $self->is_precat ) {
825 $self->script_runner->insert("environment.isPrecat", 1, 1);
826 $self->make_precat_copy;
827 return if $self->bail_out;
829 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
830 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
833 $self->do_copy_checks;
834 return if $self->bail_out;
836 $self->run_checkout_scripts();
837 return if $self->bail_out;
839 $self->build_checkout_circ_object();
840 return if $self->bail_out;
842 $self->apply_modified_due_date();
843 return if $self->bail_out;
845 return $self->bail_on_events($self->editor->event)
846 unless $self->editor->create_action_circulation($self->circ);
848 # refresh the circ to force local time zone for now
849 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
851 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
853 return if $self->bail_out;
855 $self->handle_checkout_holds();
856 return if $self->bail_out;
858 # ------------------------------------------------------------------------------
859 # Update the patron penalty info in the DB
860 # ------------------------------------------------------------------------------
861 $U->update_patron_penalties(
862 authtoken => $self->editor->authtoken,
863 patron => $self->patron,
867 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
869 OpenILS::Event->new('SUCCESS',
871 copy => $U->unflesh_copy($self->copy),
874 holds_fulfilled => $self->fulfilled_holds,
882 my $copy = $self->copy;
884 my $stat = $copy->status if ref $copy->status;
885 my $loc = $copy->location if ref $copy->location;
886 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
888 $copy->status($stat->id) if $stat;
889 $copy->location($loc->id) if $loc;
890 $copy->circ_lib($circ_lib->id) if $circ_lib;
891 $copy->editor($self->editor->requestor->id);
892 $copy->edit_date('now');
893 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
895 return $self->bail_on_events($self->editor->event)
896 unless $self->editor->update_asset_copy($self->copy);
898 $copy->status($U->copy_status($copy->status));
899 $copy->location($loc) if $loc;
900 $copy->circ_lib($circ_lib) if $circ_lib;
905 my( $self, @evts ) = @_;
906 $self->push_events(@evts);
910 sub handle_checkout_holds {
913 my $copy = $self->copy;
914 my $patron = $self->patron;
916 my $holds = $self->editor->search_action_hold_request(
918 current_copy => $copy->id ,
919 cancel_time => undef,
920 fulfillment_time => undef
926 # XXX We should only fulfill one hold here...
927 # XXX If a hold was transited to the user who is checking out
928 # the item, we need to make sure that hold is what's grabbed
931 # for now, just sort by id to get what should be the oldest hold
932 $holds = [ sort { $a->id <=> $b->id } @$holds ];
933 my @myholds = grep { $_->usr eq $patron->id } @$holds;
934 my @altholds = grep { $_->usr ne $patron->id } @$holds;
937 my $hold = $myholds[0];
939 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
941 # if the hold was never officially captured, capture it.
942 $hold->capture_time('now') unless $hold->capture_time;
944 # just make sure it's set correctly
945 $hold->current_copy($copy->id);
947 $hold->fulfillment_time('now');
948 $hold->fulfillment_staff($self->editor->requestor->id);
949 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
951 return $self->bail_on_events($self->editor->event)
952 unless $self->editor->update_action_hold_request($hold);
954 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
956 push( @fulfilled, $hold->id );
959 # If there are any holds placed for other users that point to this copy,
960 # then we need to un-target those holds so the targeter can pick a new copy
963 $logger->info("circulator: un-targeting hold ".$_->id.
964 " because copy ".$copy->id." is getting checked out");
966 # - make the targeter process this hold at next run
967 $_->clear_prev_check_time;
969 # - clear out the targetted copy
970 $_->clear_current_copy;
971 $_->clear_capture_time;
973 return $self->bail_on_event($self->editor->event)
974 unless $self->editor->update_action_hold_request($_);
978 $self->fulfilled_holds(\@fulfilled);
983 sub run_checkout_scripts {
987 my $runner = $self->script_runner;
988 $runner->load($self->circ_duration);
990 my $result = $runner->run or
991 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
993 my $duration = $result->{durationRule};
994 my $recurring = $result->{recurringFinesRule};
995 my $max_fine = $result->{maxFine};
997 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
999 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1000 return $self->bail_on_events($evt) if $evt;
1002 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1003 return $self->bail_on_events($evt) if $evt;
1005 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1006 return $self->bail_on_events($evt) if $evt;
1010 # The item circulates with an unlimited duration
1016 $self->duration_rule($duration);
1017 $self->recurring_fines_rule($recurring);
1018 $self->max_fine_rule($max_fine);
1022 sub build_checkout_circ_object {
1025 my $circ = Fieldmapper::action::circulation->new;
1026 my $duration = $self->duration_rule;
1027 my $max = $self->max_fine_rule;
1028 my $recurring = $self->recurring_fines_rule;
1029 my $copy = $self->copy;
1030 my $patron = $self->patron;
1034 my $dname = $duration->name;
1035 my $mname = $max->name;
1036 my $rname = $recurring->name;
1038 $logger->debug("circulator: building circulation ".
1039 "with duration=$dname, maxfine=$mname, recurring=$rname");
1041 $circ->duration( $duration->shrt )
1042 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1043 $circ->duration( $duration->normal )
1044 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1045 $circ->duration( $duration->extended )
1046 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1048 $circ->recuring_fine( $recurring->low )
1049 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1050 $circ->recuring_fine( $recurring->normal )
1051 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1052 $circ->recuring_fine( $recurring->high )
1053 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1055 $circ->duration_rule( $duration->name );
1056 $circ->recuring_fine_rule( $recurring->name );
1057 $circ->max_fine_rule( $max->name );
1058 $circ->max_fine( $max->amount );
1060 $circ->fine_interval($recurring->recurance_interval);
1061 $circ->renewal_remaining( $duration->max_renewals );
1065 $logger->info("circulator: copy found with an unlimited circ duration");
1066 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1067 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1068 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1069 $circ->renewal_remaining(0);
1072 $circ->target_copy( $copy->id );
1073 $circ->usr( $patron->id );
1074 $circ->circ_lib( $self->circ_lib );
1076 if( $self->is_renewal ) {
1077 $circ->opac_renewal(1);
1078 $circ->renewal_remaining($self->renewal_remaining);
1079 $circ->circ_staff($self->editor->requestor->id);
1082 # if the user provided an overiding checkout time,
1083 # (e.g. the checkout really happened several hours ago), then
1084 # we apply that here. Does this need a perm??
1085 $circ->xact_start(clense_ISO8601($self->checkout_time))
1086 if $self->checkout_time;
1088 # if a patron is renewing, 'requestor' will be the patron
1089 $circ->circ_staff($self->editor->requestor->id);
1090 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1096 sub apply_modified_due_date {
1098 my $circ = $self->circ;
1099 my $copy = $self->copy;
1101 if( $self->due_date ) {
1103 return $self->bail_on_events($self->editor->event)
1104 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1106 $circ->due_date(clense_ISO8601($self->due_date));
1110 # if the due_date lands on a day when the location is closed
1111 return unless $copy and $circ->due_date;
1113 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1115 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1116 " with an item due date of ".$circ->due_date );
1118 my $dateinfo = $U->storagereq(
1119 'open-ils.storage.actor.org_unit.closed_date.overlap',
1120 $org, $circ->due_date );
1123 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1124 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1126 # XXX make the behavior more dynamic
1127 # for now, we just push the due date to after the close date
1128 $circ->due_date($dateinfo->{end});
1135 sub create_due_date {
1136 my( $self, $duration ) = @_;
1137 my ($sec,$min,$hour,$mday,$mon,$year) =
1138 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1139 $year += 1900; $mon += 1;
1140 my $due_date = sprintf(
1141 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1142 $year, $mon, $mday, $hour, $min, $sec);
1148 sub make_precat_copy {
1150 my $copy = $self->copy;
1153 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1155 $copy->editor($self->editor->requestor->id);
1156 $copy->edit_date('now');
1157 $copy->dummy_title($self->dummy_title);
1158 $copy->dummy_author($self->dummy_author);
1160 $self->update_copy();
1164 $logger->info("circulator: Creating a new precataloged ".
1165 "copy in checkout with barcode " . $self->copy_barcode);
1167 $copy = Fieldmapper::asset::copy->new;
1168 $copy->circ_lib($self->circ_lib);
1169 $copy->creator($self->editor->requestor->id);
1170 $copy->editor($self->editor->requestor->id);
1171 $copy->barcode($self->copy_barcode);
1172 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1173 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1174 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1176 $copy->dummy_title($self->dummy_title || "");
1177 $copy->dummy_author($self->dummy_author || "");
1179 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1181 $self->push_events($self->editor->event);
1185 # this is a little bit of a hack, but we need to
1186 # get the copy into the script runner
1187 $self->script_runner->insert("environment.copy", $copy, 1);
1191 sub checkout_noncat {
1197 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1198 my $count = $self->noncat_count || 1;
1199 my $cotime = clense_ISO8601($self->checkout_time) || "";
1201 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1205 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1206 $self->editor->requestor->id,
1214 $self->push_events($evt);
1225 $self->log_me("do_checkin()");
1228 return $self->bail_on_events(
1229 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1232 if( $self->checkin_check_holds_shelf() ) {
1233 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1234 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1235 $self->checkin_flesh_events;
1239 unless( $self->is_renewal ) {
1240 return $self->bail_on_events($self->editor->event)
1241 unless $self->editor->allowed('COPY_CHECKIN');
1244 $self->push_events($self->check_copy_alert());
1245 $self->push_events($self->check_checkin_copy_status());
1247 # the renew code will have already found our circulation object
1248 unless( $self->is_renewal and $self->circ ) {
1250 $self->editor->search_action_circulation(
1251 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1254 # if the circ is marked as 'claims returned', add the event to the list
1255 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1256 if ($self->circ and $self->circ->stop_fines
1257 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1259 # handle the overridable events
1260 $self->override_events unless $self->is_renewal;
1261 return if $self->bail_out;
1265 $self->editor->search_action_transit_copy(
1266 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1270 $self->checkin_handle_circ;
1271 return if $self->bail_out;
1272 $self->checkin_changed(1);
1274 } elsif( $self->transit ) {
1275 my $hold_transit = $self->process_received_transit;
1276 $self->checkin_changed(1);
1278 if( $self->bail_out ) {
1279 $self->checkin_flesh_events;
1283 if( my $e = $self->check_checkin_copy_status() ) {
1284 # If the original copy status is special, alert the caller
1285 my $ev = $self->events;
1286 $self->events([$e]);
1287 $self->override_events;
1288 return if $self->bail_out;
1292 if( $hold_transit or
1293 $U->copy_status($self->copy->status)->id
1294 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1297 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1298 $U->fetch_open_hold_by_copy($self->copy->id)
1301 $self->checkin_flesh_events;
1305 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1306 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1307 " that is in-transit, but there is no transit.. repairing");
1308 $self->reshelve_copy(1);
1309 return if $self->bail_out;
1312 if( $self->is_renewal ) {
1313 $self->push_events(OpenILS::Event->new('SUCCESS'));
1317 # ------------------------------------------------------------------------------
1318 # Circulations and transits are now closed where necessary. Now go on to see if
1319 # this copy can fulfill a hold or needs to be routed to a different location
1320 # ------------------------------------------------------------------------------
1322 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1323 return if $self->bail_out;
1325 } else { # not needed for a hold
1328 my $circ_lib = (ref $self->copy->circ_lib) ?
1329 $self->copy->circ_lib->id : $self->copy->circ_lib;
1331 if( $self->remote_hold ) {
1332 $circ_lib = $self->remote_hold->pickup_lib;
1333 $logger->warn("circulator: Copy ".$self->copy->barcode.
1334 " is on a remote hold's shelf, sending to $circ_lib");
1337 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1339 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1341 $self->checkin_handle_precat();
1342 return if $self->bail_out;
1346 my $bc = $self->copy->barcode;
1347 $logger->info("circulator: copy $bc at a remote lib - sending home");
1348 $self->checkin_build_copy_transit();
1349 return if $self->bail_out;
1350 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1354 $self->reshelve_copy;
1355 return if $self->bail_out;
1357 unless($self->checkin_changed) {
1359 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1360 my $stat = $U->copy_status($self->copy->status)->id;
1362 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1363 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1364 $self->bail_out(1); # no need to commit anything
1367 $self->push_events(OpenILS::Event->new('SUCCESS'))
1368 unless @{$self->events};
1372 # ------------------------------------------------------------------------------
1373 # Update the patron penalty info in the DB
1374 # ------------------------------------------------------------------------------
1375 $U->update_patron_penalties(
1376 authtoken => $self->editor->authtoken,
1377 patron => $self->patron,
1378 background => 1 ) if $self->is_checkin;
1380 $self->checkin_flesh_events;
1386 my $force = $self->force || shift;
1387 my $copy = $self->copy;
1389 my $stat = $U->copy_status($copy->status)->id;
1392 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1393 $stat != OILS_COPY_STATUS_CATALOGING and
1394 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1395 $stat != OILS_COPY_STATUS_RESHELVING )) {
1397 $copy->status( OILS_COPY_STATUS_RESHELVING );
1399 $self->checkin_changed(1);
1404 # Returns true if the item is at the current location
1405 # because it was transited there for a hold and the
1406 # hold has not been fulfilled
1407 sub checkin_check_holds_shelf {
1409 return 0 unless $self->copy;
1412 $U->copy_status($self->copy->status)->id ==
1413 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1415 # find the hold that put us on the holds shelf
1416 my $holds = $self->editor->search_action_hold_request(
1418 current_copy => $self->copy->id,
1419 capture_time => { '!=' => undef },
1420 fulfillment_time => undef,
1421 cancel_time => undef,
1426 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1427 $self->reshelve_copy(1);
1431 my $hold = $$holds[0];
1433 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1434 $hold->id. "] for copy ".$self->copy->barcode);
1436 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1437 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1441 $logger->info("circulator: hold is not for here..");
1442 $self->remote_hold($hold);
1447 sub checkin_handle_precat {
1449 my $copy = $self->copy;
1451 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1452 $copy->status(OILS_COPY_STATUS_CATALOGING);
1453 $self->update_copy();
1454 $self->checkin_changed(1);
1455 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1460 sub checkin_build_copy_transit {
1462 my $copy = $self->copy;
1463 my $transit = Fieldmapper::action::transit_copy->new;
1465 $transit->source($self->editor->requestor->ws_ou);
1466 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1467 $transit->target_copy($copy->id);
1468 $transit->source_send_time('now');
1469 $transit->copy_status( $U->copy_status($copy->status)->id );
1471 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1473 return $self->bail_on_events($self->editor->event)
1474 unless $self->editor->create_action_transit_copy($transit);
1476 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1478 $self->checkin_changed(1);
1482 sub attempt_checkin_hold_capture {
1484 my $copy = $self->copy;
1486 # See if this copy can fulfill any holds
1487 my ($hold) = $holdcode->find_nearest_permitted_hold(
1488 OpenSRF::AppSession->create('open-ils.storage'),
1489 $copy, $self->editor->requestor );
1492 $logger->debug("circulator: no potential permitted".
1493 "holds found for copy ".$copy->barcode);
1498 $logger->info("circulator: found permitted hold ".
1499 $hold->id . " for copy, capturing...");
1501 $hold->current_copy($copy->id);
1502 $hold->capture_time('now');
1504 # prevent DB errors caused by fetching
1505 # holds from storage, and updating through cstore
1506 $hold->clear_fulfillment_time;
1507 $hold->clear_fulfillment_staff;
1508 $hold->clear_fulfillment_lib;
1509 $hold->clear_expire_time;
1510 $hold->clear_cancel_time;
1511 $hold->clear_prev_check_time unless $hold->prev_check_time;
1513 $self->bail_on_events($self->editor->event)
1514 unless $self->editor->update_action_hold_request($hold);
1516 $self->checkin_changed(1);
1518 return 1 if $self->bail_out;
1520 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1522 # This hold was captured in the correct location
1523 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1524 $self->push_events(OpenILS::Event->new('SUCCESS'));
1526 #$self->do_hold_notify($hold->id);
1527 $self->notify_hold($hold->id);
1531 # Hold needs to be picked up elsewhere. Build a hold
1532 # transit and route the item.
1533 $self->checkin_build_hold_transit();
1534 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1535 return 1 if $self->bail_out;
1537 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1540 # make sure we save the copy status
1545 sub do_hold_notify {
1546 my( $self, $holdid ) = @_;
1548 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1549 editor => $self->editor, hold_id => $holdid );
1551 if(!$notifier->event) {
1553 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1555 my $stat = $notifier->send_email_notify;
1556 if( $stat == '1' ) {
1557 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1558 $self->editor->commit;
1562 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1565 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1568 $self->editor->rollback;
1572 sub checkin_build_hold_transit {
1575 my $copy = $self->copy;
1576 my $hold = $self->hold;
1577 my $trans = Fieldmapper::action::hold_transit_copy->new;
1579 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1581 $trans->hold($hold->id);
1582 $trans->source($self->editor->requestor->ws_ou);
1583 $trans->dest($hold->pickup_lib);
1584 $trans->source_send_time("now");
1585 $trans->target_copy($copy->id);
1587 # when the copy gets to its destination, it will recover
1588 # this status - put it onto the holds shelf
1589 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1591 return $self->bail_on_events($self->editor->event)
1592 unless $self->editor->create_action_hold_transit_copy($trans);
1597 sub process_received_transit {
1599 my $copy = $self->copy;
1600 my $copyid = $self->copy->id;
1602 my $status_name = $U->copy_status($copy->status)->name;
1603 $logger->debug("circulator: attempting transit receive on ".
1604 "copy $copyid. Copy status is $status_name");
1606 my $transit = $self->transit;
1608 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1609 $logger->info("circulator: Fowarding transit on copy which is destined ".
1610 "for a different location. copy=$copyid,current ".
1611 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1613 return $self->bail_on_events(
1614 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1617 # The transit is received, set the receive time
1618 $transit->dest_recv_time('now');
1619 $self->bail_on_events($self->editor->event)
1620 unless $self->editor->update_action_transit_copy($transit);
1622 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1624 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1625 $copy->status( $transit->copy_status );
1626 $self->update_copy();
1627 return if $self->bail_out;
1631 #$self->do_hold_notify($hold_transit->hold);
1632 $self->notify_hold($hold_transit->hold);
1637 OpenILS::Event->new(
1640 payload => { transit => $transit, holdtransit => $hold_transit } ));
1642 return $hold_transit;
1646 sub checkin_handle_circ {
1650 my $circ = $self->circ;
1651 my $copy = $self->copy;
1655 # backdate the circ if necessary
1656 if($self->backdate) {
1657 $self->checkin_handle_backdate;
1658 return if $self->bail_out;
1661 if(!$circ->stop_fines) {
1662 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1663 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1664 $circ->stop_fines_time('now') unless $self->backdate;
1665 $circ->stop_fines_time($self->backdate) if $self->backdate;
1668 # see if there are any fines owed on this circ. if not, close it
1669 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1670 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1672 # Set the checkin vars since we have the item
1673 $circ->checkin_time('now');
1674 $circ->checkin_staff($self->editor->requestor->id);
1675 $circ->checkin_lib($self->editor->requestor->ws_ou);
1677 my $circ_lib = (ref $self->copy->circ_lib) ?
1678 $self->copy->circ_lib->id : $self->copy->circ_lib;
1679 my $stat = $U->copy_status($self->copy->status)->id;
1681 # If the item is lost/missing and it needs to be sent home, don't
1682 # reshelve the copy, leave it lost/missing so the recipient will know
1683 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1684 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1685 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1688 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1693 return $self->bail_on_events($self->editor->event)
1694 unless $self->editor->update_action_circulation($circ);
1698 sub checkin_handle_backdate {
1701 my $bd = $self->backdate;
1702 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1703 $bd = "${bd}T23:59:59";
1705 my $bills = $self->editor->search_money_billing(
1707 billing_ts => { '>=' => $bd },
1708 xact => $self->circ->id,
1709 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1713 for my $bill (@$bills) {
1714 if( !$bill->voided or $bill->voided =~ /f/i ) {
1716 my $n = $bill->note || "";
1717 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1719 $self->bail_on_events($self->editor->event)
1720 unless $self->editor->update_money_billing($bill);
1727 # XXX Legacy version for Circ.pm support
1728 sub _checkin_handle_backdate {
1729 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1732 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1733 $bd = "${bd}T23:59:59";
1736 my $bills = $session->request(
1737 "open-ils.storage.direct.money.billing.search_where.atomic",
1738 billing_ts => { '>=' => $bd },
1740 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1744 for my $bill (@$bills) {
1746 my $n = $bill->note || "";
1747 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1748 my $s = $session->request(
1749 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1750 return $U->DB_UPDATE_FAILED($bill) unless $s;
1760 sub find_patron_from_copy {
1762 my $circs = $self->editor->search_action_circulation(
1763 { target_copy => $self->copy->id, checkin_time => undef });
1764 my $circ = $circs->[0];
1765 return unless $circ;
1766 my $u = $self->editor->retrieve_actor_user($circ->usr)
1767 or return $self->bail_on_events($self->editor->event);
1771 sub check_checkin_copy_status {
1773 my $copy = $self->copy;
1779 my $status = $U->copy_status($copy->status)->id;
1782 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1783 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1784 $status == OILS_COPY_STATUS_IN_PROCESS ||
1785 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1786 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1787 $status == OILS_COPY_STATUS_CATALOGING ||
1788 $status == OILS_COPY_STATUS_RESHELVING );
1790 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1791 if( $status == OILS_COPY_STATUS_LOST );
1793 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1794 if( $status == OILS_COPY_STATUS_MISSING );
1796 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1801 # --------------------------------------------------------------------------
1802 # On checkin, we need to return as many relevant objects as we can
1803 # --------------------------------------------------------------------------
1804 sub checkin_flesh_events {
1807 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1808 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1809 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1813 for my $evt (@{$self->events}) {
1816 $payload->{copy} = $U->unflesh_copy($self->copy);
1817 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1818 $payload->{circ} = $self->circ;
1819 $payload->{transit} = $self->transit;
1820 $payload->{hold} = $self->hold;
1822 $evt->{payload} = $payload;
1827 my( $self, $msg ) = @_;
1828 my $bc = ($self->copy) ? $self->copy->barcode :
1831 my $usr = ($self->patron) ? $self->patron->id : "";
1832 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1833 ", recipient=$usr, copy=$bc");
1839 $self->log_me("do_renew()");
1840 $self->is_renewal(1);
1842 unless( $self->is_renewal ) {
1843 return $self->bail_on_events($self->editor->events)
1844 unless $self->editor->allowed('RENEW_CIRC');
1847 # Make sure there is an open circ to renew that is not
1848 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1849 my $circ = $self->editor->search_action_circulation(
1850 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1853 $circ = $self->editor->search_action_circulation(
1855 target_copy => $self->copy->id,
1856 stop_fines => OILS_STOP_FINES_MAX_FINES,
1857 checkin_time => undef
1862 return $self->bail_on_events($self->editor->event) unless $circ;
1864 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1865 if $circ->renewal_remaining < 1;
1867 # -----------------------------------------------------------------
1869 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1872 $self->run_renew_permit;
1875 $self->do_checkin();
1876 return if $self->bail_out;
1878 unless( $self->permit_override ) {
1880 return if $self->bail_out;
1881 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1882 $self->remove_event('ITEM_NOT_CATALOGED');
1885 $self->override_events;
1886 return if $self->bail_out;
1889 $self->do_checkout();
1894 my( $self, $evt ) = @_;
1895 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1896 $logger->debug("circulator: removing event from list: $evt");
1897 my @events = @{$self->events};
1898 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1903 my( $self, $evt ) = @_;
1904 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1905 return grep { $_->{textcode} eq $evt } @{$self->events};
1910 sub run_renew_permit {
1912 my $runner = $self->script_runner;
1914 $runner->load($self->circ_permit_renew);
1915 my $result = $runner->run or
1916 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1917 my $events = $result->{events};
1919 $logger->activity("ciculator: circ_permit_renew for user ".
1920 $self->patron->id." returned events: @$events") if @$events;
1922 $self->push_events(OpenILS::Event->new($_)) for @$events;
1924 $logger->debug("circulator: re-creating script runner to be safe");
1925 $self->mk_script_runner;