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};
265 # --------------------------------------------------------------------------
266 # This package actually manages all of the circulation logic
267 # --------------------------------------------------------------------------
268 package OpenILS::Application::Circ::Circulator;
269 use strict; use warnings;
270 use vars q/$AUTOLOAD/;
272 use OpenILS::Utils::Fieldmapper;
273 use OpenSRF::Utils::Cache;
274 use Digest::MD5 qw(md5_hex);
275 use DateTime::Format::ISO8601;
276 use OpenILS::Utils::PermitHold;
277 use OpenSRF::Utils qw/:datetime/;
278 use OpenSRF::Utils::SettingsClient;
279 use OpenILS::Application::Circ::Holds;
280 use OpenILS::Application::Circ::Transit;
281 use OpenSRF::Utils::Logger qw(:logger);
282 use OpenILS::Utils::CStoreEditor qw/:funcs/;
283 use OpenILS::Application::Circ::ScriptBuilder;
284 use OpenILS::Const qw/:const/;
286 my $U = "OpenILS::Application::AppUtils";
287 my $holdcode = "OpenILS::Application::Circ::Holds";
288 my $transcode = "OpenILS::Application::Circ::Transit";
293 # --------------------------------------------------------------------------
294 # Add a pile of automagic getter/setter methods
295 # --------------------------------------------------------------------------
296 my @AUTOLOAD_FIELDS = qw/
337 recurring_fines_level
350 cancelled_hold_transit
359 my $type = ref($self) or die "$self is not an object";
361 my $name = $AUTOLOAD;
364 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
365 $logger->error("circulator: $type: invalid autoload field: $name");
366 die "$type: invalid autoload field: $name\n"
371 *{"${type}::${name}"} = sub {
374 $s->{$name} = $v if defined $v;
378 return $self->$name($data);
383 my( $class, $auth, %args ) = @_;
384 $class = ref($class) || $class;
385 my $self = bless( {}, $class );
389 new_editor(xact => 1, authtoken => $auth) );
391 unless( $self->editor->checkauth ) {
392 $self->bail_on_events($self->editor->event);
396 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
398 $self->$_($args{$_}) for keys %args;
401 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
403 # if this is a renewal, default to desk_renewal
404 $self->desk_renewal(1) unless
405 $self->opac_renewal or $self->phone_renewal;
411 # --------------------------------------------------------------------------
412 # True if we should discontinue processing
413 # --------------------------------------------------------------------------
415 my( $self, $bool ) = @_;
416 if( defined $bool ) {
417 $logger->info("circulator: BAILING OUT") if $bool;
418 $self->{bail_out} = $bool;
420 return $self->{bail_out};
425 my( $self, @evts ) = @_;
428 $logger->info("circulator: pushing event ".$e->{textcode});
429 push( @{$self->events}, $e ) unless
430 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
436 my $key = md5_hex( time() . rand() . "$$" );
437 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
438 return $self->permit_key($key);
441 sub check_permit_key {
443 my $key = $self->permit_key;
444 return 0 unless $key;
445 my $k = "oils_permit_key_$key";
446 my $one = $self->cache_handle->get_cache($k);
447 $self->cache_handle->delete_cache($k);
448 return ($one) ? 1 : 0;
452 # --------------------------------------------------------------------------
453 # This builds the script runner environment and fetches most of the
455 # --------------------------------------------------------------------------
456 sub mk_script_runner {
462 qw/copy copy_barcode copy_id patron
463 patron_id patron_barcode volume title editor/;
465 # Translate our objects into the ScriptBuilder args hash
466 $$args{$_} = $self->$_() for @fields;
468 $args->{ignore_user_status} = 1 if $self->is_checkin;
469 $$args{fetch_patron_by_circ_copy} = 1;
470 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
472 if( my $pco = $self->pending_checkouts ) {
473 $logger->info("circulator: we were given a pending checkouts number of $pco");
474 $$args{patronItemsOut} = $pco;
477 # This fetches most of the objects we need
478 $self->script_runner(
479 OpenILS::Application::Circ::ScriptBuilder->build($args));
481 # Now we translate the ScriptBuilder objects back into self
482 $self->$_($$args{$_}) for @fields;
484 my @evts = @{$args->{_events}} if $args->{_events};
486 $logger->debug("circulator: script builder returned events: @evts") if @evts;
490 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
491 if(!$self->is_noncat and
493 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
497 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
498 return $self->bail_on_events(@e);
502 $self->is_precat(1) if $self->copy
503 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
505 # We can't renew if there is no copy
506 return $self->bail_on_events(@evts) if
507 $self->is_renewal and !$self->copy;
509 # Set some circ-specific flags in the script environment
510 my $evt = "environment";
511 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
513 if( $self->is_noncat ) {
514 $self->script_runner->insert("$evt.isNonCat", 1);
515 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
518 if( $self->is_precat ) {
519 $self->script_runner->insert("environment.isPrecat", 1, 1);
522 $self->script_runner->add_path( $_ ) for @$script_libs;
530 # --------------------------------------------------------------------------
531 # Does the circ permit work
532 # --------------------------------------------------------------------------
536 $self->log_me("do_permit()");
538 unless( $self->editor->requestor->id == $self->patron->id ) {
539 return $self->bail_on_events($self->editor->event)
540 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
543 $self->check_captured_holds();
544 $self->do_copy_checks();
545 return if $self->bail_out;
546 $self->run_patron_permit_scripts();
547 $self->run_copy_permit_scripts()
548 unless $self->is_precat or $self->is_noncat;
549 $self->override_events() unless $self->is_renewal;
550 return if $self->bail_out;
552 if( $self->is_precat ) {
555 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
556 return $self->bail_out(1) unless $self->is_renewal;
562 payload => $self->mk_permit_key));
566 sub check_captured_holds {
568 my $copy = $self->copy;
569 my $patron = $self->patron;
571 return undef unless $copy;
573 my $s = $U->copy_status($copy->status)->id;
574 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
575 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
577 # Item is on the holds shelf, make sure it's going to the right person
578 my $holds = $self->editor->search_action_hold_request(
581 current_copy => $copy->id ,
582 capture_time => { '!=' => undef },
583 cancel_time => undef,
584 fulfillment_time => undef
590 if( $holds and $$holds[0] ) {
591 return undef if $$holds[0]->usr == $patron->id;
594 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
596 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
602 my $copy = $self->copy;
605 my $stat = $U->copy_status($copy->status)->id;
607 # We cannot check out a copy if it is in-transit
608 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
609 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
612 $self->handle_claims_returned();
613 return if $self->bail_out;
615 # no claims returned circ was found, check if there is any open circ
616 unless( $self->is_renewal ) {
617 my $circs = $self->editor->search_action_circulation(
618 { target_copy => $copy->id, checkin_time => undef }
621 return $self->bail_on_events(
622 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
627 sub send_penalty_request {
629 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
630 $self->penalty_request(
632 'open-ils.penalty.patron_penalty.calculate',
634 authtoken => $self->editor->authtoken,
635 patron => $self->patron } ) );
638 sub gather_penalty_request {
640 return [] unless $self->penalty_request;
641 my $data = $self->penalty_request->recv;
643 throw $data if UNIVERSAL::isa($data,'Error');
644 $data = $data->content;
645 return $data->{fatal_penalties};
647 $logger->error("circulator: penalty request returned no data");
651 # ---------------------------------------------------------------------
652 # This pushes any patron-related events into the list but does not
653 # set bail_out for any events
654 # ---------------------------------------------------------------------
655 sub run_patron_permit_scripts {
657 my $runner = $self->script_runner;
658 my $patronid = $self->patron->id;
660 $self->send_penalty_request() unless $self->is_renewal;
662 # ---------------------------------------------------------------------
663 # Now run the patron permit script
664 # ---------------------------------------------------------------------
665 $runner->load($self->circ_permit_patron);
666 my $result = $runner->run or
667 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
669 my $patron_events = $result->{events};
673 # ---------------------------------------------------------------------
674 # this is policy directly in the code, not a good idea in general, but
675 # the penalty server doesn't know anything about renewals, so we
676 # have to strip the event out here
677 my $penalties = ($self->is_renewal) ? [] : $self->gather_penalty_request();
678 # ---------------------------------------------------------------------
680 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
682 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
684 $self->push_events(@allevents);
688 sub run_copy_permit_scripts {
690 my $copy = $self->copy || return;
691 my $runner = $self->script_runner;
693 # ---------------------------------------------------------------------
694 # Capture all of the copy permit events
695 # ---------------------------------------------------------------------
696 $runner->load($self->circ_permit_copy);
697 my $result = $runner->run or
698 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
699 my $copy_events = $result->{events};
701 # ---------------------------------------------------------------------
702 # Now collect all of the events together
703 # ---------------------------------------------------------------------
705 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
707 # See if this copy has an alert message
708 my $ae = $self->check_copy_alert();
709 push( @allevents, $ae ) if $ae;
711 # uniquify the events
712 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
713 @allevents = values %hash;
716 $_->{payload} = $copy if
717 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
720 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
722 $self->push_events(@allevents);
726 sub check_copy_alert {
728 return undef if $self->is_renewal;
729 return OpenILS::Event->new(
730 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
731 if $self->copy and $self->copy->alert_message;
737 # --------------------------------------------------------------------------
738 # If the call is overriding and has permissions to override every collected
739 # event, the are cleared. Any event that the caller does not have
740 # permission to override, will be left in the event list and bail_out will
742 # XXX We need code in here to cancel any holds/transits on copies
743 # that are being force-checked out
744 # --------------------------------------------------------------------------
745 sub override_events {
747 my @events = @{$self->events};
748 return unless @events;
750 if(!$self->override) {
751 return $self->bail_out(1)
752 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
757 for my $e (@events) {
758 my $tc = $e->{textcode};
759 next if $tc eq 'SUCCESS';
760 my $ov = "$tc.override";
761 $logger->info("circulator: attempting to override event: $ov");
763 return $self->bail_on_events($self->editor->event)
764 unless( $self->editor->allowed($ov) );
769 # --------------------------------------------------------------------------
770 # If there is an open claimsreturn circ on the requested copy, close the
771 # circ if overriding, otherwise bail out
772 # --------------------------------------------------------------------------
773 sub handle_claims_returned {
775 my $copy = $self->copy;
777 my $CR = $self->editor->search_action_circulation(
779 target_copy => $copy->id,
780 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
781 checkin_time => undef,
785 return unless ($CR = $CR->[0]);
789 # - If the caller has set the override flag, we will check the item in
790 if($self->override) {
792 $CR->checkin_time('now');
793 $CR->checkin_lib($self->editor->requestor->ws_ou);
794 $CR->checkin_staff($self->editor->requestor->id);
796 $evt = $self->editor->event
797 unless $self->editor->update_action_circulation($CR);
800 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
803 $self->bail_on_events($evt) if $evt;
808 # --------------------------------------------------------------------------
809 # This performs the checkout
810 # --------------------------------------------------------------------------
814 $self->log_me("do_checkout()");
816 # make sure perms are good if this isn't a renewal
817 unless( $self->is_renewal ) {
818 return $self->bail_on_events($self->editor->event)
819 unless( $self->editor->allowed('COPY_CHECKOUT') );
822 # verify the permit key
823 unless( $self->check_permit_key ) {
824 if( $self->permit_override ) {
825 return $self->bail_on_events($self->editor->event)
826 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
828 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
832 # if this is a non-cataloged circ, build the circ and finish
833 if( $self->is_noncat ) {
834 $self->checkout_noncat;
836 OpenILS::Event->new('SUCCESS',
837 payload => { noncat_circ => $self->circ }));
841 if( $self->is_precat ) {
842 $self->script_runner->insert("environment.isPrecat", 1, 1);
843 $self->make_precat_copy;
844 return if $self->bail_out;
846 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
847 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
850 $self->do_copy_checks;
851 return if $self->bail_out;
853 $self->run_checkout_scripts();
854 return if $self->bail_out;
856 $self->build_checkout_circ_object();
857 return if $self->bail_out;
859 $self->apply_modified_due_date();
860 return if $self->bail_out;
862 return $self->bail_on_events($self->editor->event)
863 unless $self->editor->create_action_circulation($self->circ);
865 # refresh the circ to force local time zone for now
866 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
868 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
870 return if $self->bail_out;
872 $self->handle_checkout_holds();
873 return if $self->bail_out;
875 # ------------------------------------------------------------------------------
876 # Update the patron penalty info in the DB. Run it for permit-overrides or
877 # renewals since both of those cases do not require the penalty server to
878 # run during the permit phase of the checkout
879 # ------------------------------------------------------------------------------
880 if( $self->permit_override or $self->is_renewal ) {
881 $U->update_patron_penalties(
882 authtoken => $self->editor->authtoken,
883 patron => $self->patron,
888 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
890 OpenILS::Event->new('SUCCESS',
892 copy => $U->unflesh_copy($self->copy),
895 holds_fulfilled => $self->fulfilled_holds,
903 my $copy = $self->copy;
905 my $stat = $copy->status if ref $copy->status;
906 my $loc = $copy->location if ref $copy->location;
907 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
909 $copy->status($stat->id) if $stat;
910 $copy->location($loc->id) if $loc;
911 $copy->circ_lib($circ_lib->id) if $circ_lib;
912 $copy->editor($self->editor->requestor->id);
913 $copy->edit_date('now');
914 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
916 return $self->bail_on_events($self->editor->event)
917 unless $self->editor->update_asset_copy($self->copy);
919 $copy->status($U->copy_status($copy->status));
920 $copy->location($loc) if $loc;
921 $copy->circ_lib($circ_lib) if $circ_lib;
926 my( $self, @evts ) = @_;
927 $self->push_events(@evts);
931 sub handle_checkout_holds {
934 my $copy = $self->copy;
935 my $patron = $self->patron;
937 my $holds = $self->editor->search_action_hold_request(
939 current_copy => $copy->id ,
940 cancel_time => undef,
941 fulfillment_time => undef
947 # XXX We should only fulfill one hold here...
948 # XXX If a hold was transited to the user who is checking out
949 # the item, we need to make sure that hold is what's grabbed
952 # for now, just sort by id to get what should be the oldest hold
953 $holds = [ sort { $a->id <=> $b->id } @$holds ];
954 my @myholds = grep { $_->usr eq $patron->id } @$holds;
955 my @altholds = grep { $_->usr ne $patron->id } @$holds;
958 my $hold = $myholds[0];
960 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
962 # if the hold was never officially captured, capture it.
963 $hold->capture_time('now') unless $hold->capture_time;
965 # just make sure it's set correctly
966 $hold->current_copy($copy->id);
968 $hold->fulfillment_time('now');
969 $hold->fulfillment_staff($self->editor->requestor->id);
970 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
972 return $self->bail_on_events($self->editor->event)
973 unless $self->editor->update_action_hold_request($hold);
975 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
977 push( @fulfilled, $hold->id );
980 # If there are any holds placed for other users that point to this copy,
981 # then we need to un-target those holds so the targeter can pick a new copy
984 $logger->info("circulator: un-targeting hold ".$_->id.
985 " because copy ".$copy->id." is getting checked out");
987 # - make the targeter process this hold at next run
988 $_->clear_prev_check_time;
990 # - clear out the targetted copy
991 $_->clear_current_copy;
992 $_->clear_capture_time;
994 return $self->bail_on_event($self->editor->event)
995 unless $self->editor->update_action_hold_request($_);
999 $self->fulfilled_holds(\@fulfilled);
1004 sub run_checkout_scripts {
1008 my $runner = $self->script_runner;
1009 $runner->load($self->circ_duration);
1011 my $result = $runner->run or
1012 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1014 my $duration = $result->{durationRule};
1015 my $recurring = $result->{recurringFinesRule};
1016 my $max_fine = $result->{maxFine};
1018 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1020 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1021 return $self->bail_on_events($evt) if $evt;
1023 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1024 return $self->bail_on_events($evt) if $evt;
1026 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1027 return $self->bail_on_events($evt) if $evt;
1031 # The item circulates with an unlimited duration
1037 $self->duration_rule($duration);
1038 $self->recurring_fines_rule($recurring);
1039 $self->max_fine_rule($max_fine);
1043 sub build_checkout_circ_object {
1046 my $circ = Fieldmapper::action::circulation->new;
1047 my $duration = $self->duration_rule;
1048 my $max = $self->max_fine_rule;
1049 my $recurring = $self->recurring_fines_rule;
1050 my $copy = $self->copy;
1051 my $patron = $self->patron;
1055 my $dname = $duration->name;
1056 my $mname = $max->name;
1057 my $rname = $recurring->name;
1059 $logger->debug("circulator: building circulation ".
1060 "with duration=$dname, maxfine=$mname, recurring=$rname");
1062 $circ->duration( $duration->shrt )
1063 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1064 $circ->duration( $duration->normal )
1065 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1066 $circ->duration( $duration->extended )
1067 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1069 $circ->recuring_fine( $recurring->low )
1070 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1071 $circ->recuring_fine( $recurring->normal )
1072 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1073 $circ->recuring_fine( $recurring->high )
1074 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1076 $circ->duration_rule( $duration->name );
1077 $circ->recuring_fine_rule( $recurring->name );
1078 $circ->max_fine_rule( $max->name );
1079 $circ->max_fine( $max->amount );
1081 $circ->fine_interval($recurring->recurance_interval);
1082 $circ->renewal_remaining( $duration->max_renewals );
1086 $logger->info("circulator: copy found with an unlimited circ duration");
1087 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1088 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1089 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1090 $circ->renewal_remaining(0);
1093 $circ->target_copy( $copy->id );
1094 $circ->usr( $patron->id );
1095 $circ->circ_lib( $self->circ_lib );
1097 if( $self->is_renewal ) {
1098 $circ->opac_renewal('t') if $self->opac_renewal;
1099 $circ->phone_renewal('t') if $self->phone_renewal;
1100 $circ->desk_renewal('t') if $self->desk_renewal;
1101 $circ->renewal_remaining($self->renewal_remaining);
1102 $circ->circ_staff($self->editor->requestor->id);
1106 # if the user provided an overiding checkout time,
1107 # (e.g. the checkout really happened several hours ago), then
1108 # we apply that here. Does this need a perm??
1109 $circ->xact_start(clense_ISO8601($self->checkout_time))
1110 if $self->checkout_time;
1112 # if a patron is renewing, 'requestor' will be the patron
1113 $circ->circ_staff($self->editor->requestor->id);
1114 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1120 sub apply_modified_due_date {
1122 my $circ = $self->circ;
1123 my $copy = $self->copy;
1125 if( $self->due_date ) {
1127 return $self->bail_on_events($self->editor->event)
1128 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1130 $circ->due_date(clense_ISO8601($self->due_date));
1134 # if the due_date lands on a day when the location is closed
1135 return unless $copy and $circ->due_date;
1137 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1139 # due-date overlap should be determined by the location the item
1140 # is checked out from, not the owning or circ lib of the item
1141 my $org = $self->editor->requestor->ws_ou;
1143 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1144 " with an item due date of ".$circ->due_date );
1146 my $dateinfo = $U->storagereq(
1147 'open-ils.storage.actor.org_unit.closed_date.overlap',
1148 $org, $circ->due_date );
1151 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1152 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1154 # XXX make the behavior more dynamic
1155 # for now, we just push the due date to after the close date
1156 $circ->due_date($dateinfo->{end});
1163 sub create_due_date {
1164 my( $self, $duration ) = @_;
1165 my ($sec,$min,$hour,$mday,$mon,$year) =
1166 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1167 $year += 1900; $mon += 1;
1168 my $due_date = sprintf(
1169 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1170 $year, $mon, $mday, $hour, $min, $sec);
1176 sub make_precat_copy {
1178 my $copy = $self->copy;
1181 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1183 $copy->editor($self->editor->requestor->id);
1184 $copy->edit_date('now');
1185 $copy->dummy_title($self->dummy_title);
1186 $copy->dummy_author($self->dummy_author);
1188 $self->update_copy();
1192 $logger->info("circulator: Creating a new precataloged ".
1193 "copy in checkout with barcode " . $self->copy_barcode);
1195 $copy = Fieldmapper::asset::copy->new;
1196 $copy->circ_lib($self->circ_lib);
1197 $copy->creator($self->editor->requestor->id);
1198 $copy->editor($self->editor->requestor->id);
1199 $copy->barcode($self->copy_barcode);
1200 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1201 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1202 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1204 $copy->dummy_title($self->dummy_title || "");
1205 $copy->dummy_author($self->dummy_author || "");
1207 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1209 $self->push_events($self->editor->event);
1213 # this is a little bit of a hack, but we need to
1214 # get the copy into the script runner
1215 $self->script_runner->insert("environment.copy", $copy, 1);
1219 sub checkout_noncat {
1225 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1226 my $count = $self->noncat_count || 1;
1227 my $cotime = clense_ISO8601($self->checkout_time) || "";
1229 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1233 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1234 $self->editor->requestor->id,
1242 $self->push_events($evt);
1253 $self->log_me("do_checkin()");
1256 return $self->bail_on_events(
1257 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1260 if( $self->checkin_check_holds_shelf() ) {
1261 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1262 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1263 $self->checkin_flesh_events;
1267 unless( $self->is_renewal ) {
1268 return $self->bail_on_events($self->editor->event)
1269 unless $self->editor->allowed('COPY_CHECKIN');
1272 $self->push_events($self->check_copy_alert());
1273 $self->push_events($self->check_checkin_copy_status());
1275 # the renew code will have already found our circulation object
1276 unless( $self->is_renewal and $self->circ ) {
1277 my $circs = $self->editor->search_action_circulation(
1278 { target_copy => $self->copy->id, checkin_time => undef });
1279 $self->circ($$circs[0]);
1281 # for now, just warn if there are multiple open circs on a copy
1282 $logger->warn("circulator: we have ".scalar(@$circs).
1283 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1286 # if the circ is marked as 'claims returned', add the event to the list
1287 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1288 if ($self->circ and $self->circ->stop_fines
1289 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1291 # handle the overridable events
1292 $self->override_events unless $self->is_renewal;
1293 return if $self->bail_out;
1297 $self->editor->search_action_transit_copy(
1298 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1302 $self->checkin_handle_circ;
1303 return if $self->bail_out;
1304 $self->checkin_changed(1);
1306 } elsif( $self->transit ) {
1307 my $hold_transit = $self->process_received_transit;
1308 $self->checkin_changed(1);
1310 if( $self->bail_out ) {
1311 $self->checkin_flesh_events;
1315 if( my $e = $self->check_checkin_copy_status() ) {
1316 # If the original copy status is special, alert the caller
1317 my $ev = $self->events;
1318 $self->events([$e]);
1319 $self->override_events;
1320 return if $self->bail_out;
1324 if( $hold_transit or
1325 $U->copy_status($self->copy->status)->id
1326 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1329 if( $hold_transit ) {
1330 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1332 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1337 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1339 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1340 $self->reshelve_copy(1);
1341 $self->cancelled_hold_transit(1);
1342 return if $self->bail_out;
1346 # hold transited to correct location
1347 $self->checkin_flesh_events;
1352 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1354 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1355 " that is in-transit, but there is no transit.. repairing");
1356 $self->reshelve_copy(1);
1357 return if $self->bail_out;
1360 if( $self->is_renewal ) {
1361 $self->push_events(OpenILS::Event->new('SUCCESS'));
1365 # ------------------------------------------------------------------------------
1366 # Circulations and transits are now closed where necessary. Now go on to see if
1367 # this copy can fulfill a hold or needs to be routed to a different location
1368 # ------------------------------------------------------------------------------
1370 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1371 return if $self->bail_out;
1373 } else { # not needed for a hold
1375 my $circ_lib = (ref $self->copy->circ_lib) ?
1376 $self->copy->circ_lib->id : $self->copy->circ_lib;
1378 if( $self->remote_hold ) {
1379 $circ_lib = $self->remote_hold->pickup_lib;
1380 $logger->warn("circulator: Copy ".$self->copy->barcode.
1381 " is on a remote hold's shelf, sending to $circ_lib");
1384 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1386 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1388 $self->checkin_handle_precat();
1389 return if $self->bail_out;
1393 my $bc = $self->copy->barcode;
1394 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1395 $self->checkin_build_copy_transit($circ_lib);
1396 return if $self->bail_out;
1397 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1401 $self->reshelve_copy;
1402 return if $self->bail_out;
1404 unless($self->checkin_changed) {
1406 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1407 my $stat = $U->copy_status($self->copy->status)->id;
1409 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1410 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1411 $self->bail_out(1); # no need to commit anything
1415 $self->push_events(OpenILS::Event->new('SUCCESS'))
1416 unless @{$self->events};
1420 # ------------------------------------------------------------------------------
1421 # Update the patron penalty info in the DB
1422 # ------------------------------------------------------------------------------
1423 $U->update_patron_penalties(
1424 authtoken => $self->editor->authtoken,
1425 patron => $self->patron,
1426 background => 1 ) if $self->is_checkin;
1428 $self->checkin_flesh_events;
1434 my $force = $self->force || shift;
1435 my $copy = $self->copy;
1437 my $stat = $U->copy_status($copy->status)->id;
1440 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1441 $stat != OILS_COPY_STATUS_CATALOGING and
1442 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1443 $stat != OILS_COPY_STATUS_RESHELVING )) {
1445 $copy->status( OILS_COPY_STATUS_RESHELVING );
1447 $self->checkin_changed(1);
1452 # Returns true if the item is at the current location
1453 # because it was transited there for a hold and the
1454 # hold has not been fulfilled
1455 sub checkin_check_holds_shelf {
1457 return 0 unless $self->copy;
1460 $U->copy_status($self->copy->status)->id ==
1461 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1463 # find the hold that put us on the holds shelf
1464 my $holds = $self->editor->search_action_hold_request(
1466 current_copy => $self->copy->id,
1467 capture_time => { '!=' => undef },
1468 fulfillment_time => undef,
1469 cancel_time => undef,
1474 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1475 $self->reshelve_copy(1);
1479 my $hold = $$holds[0];
1481 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1482 $hold->id. "] for copy ".$self->copy->barcode);
1484 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1485 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1489 $logger->info("circulator: hold is not for here..");
1490 $self->remote_hold($hold);
1495 sub checkin_handle_precat {
1497 my $copy = $self->copy;
1499 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1500 $copy->status(OILS_COPY_STATUS_CATALOGING);
1501 $self->update_copy();
1502 $self->checkin_changed(1);
1503 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1508 sub checkin_build_copy_transit {
1511 my $copy = $self->copy;
1512 my $transit = Fieldmapper::action::transit_copy->new;
1514 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1515 $logger->info("circulator: transiting copy to $dest");
1517 $transit->source($self->editor->requestor->ws_ou);
1518 $transit->dest($dest);
1519 $transit->target_copy($copy->id);
1520 $transit->source_send_time('now');
1521 $transit->copy_status( $U->copy_status($copy->status)->id );
1523 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1525 return $self->bail_on_events($self->editor->event)
1526 unless $self->editor->create_action_transit_copy($transit);
1528 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1530 $self->checkin_changed(1);
1534 sub attempt_checkin_hold_capture {
1536 my $copy = $self->copy;
1538 # See if this copy can fulfill any holds
1539 my ($hold) = $holdcode->find_nearest_permitted_hold(
1540 $self->editor, $copy, $self->editor->requestor );
1543 $logger->debug("circulator: no potential permitted".
1544 "holds found for copy ".$copy->barcode);
1549 $logger->info("circulator: found permitted hold ".
1550 $hold->id . " for copy, capturing...");
1552 $hold->current_copy($copy->id);
1553 $hold->capture_time('now');
1555 # prevent DB errors caused by fetching
1556 # holds from storage, and updating through cstore
1557 $hold->clear_fulfillment_time;
1558 $hold->clear_fulfillment_staff;
1559 $hold->clear_fulfillment_lib;
1560 $hold->clear_expire_time;
1561 $hold->clear_cancel_time;
1562 $hold->clear_prev_check_time unless $hold->prev_check_time;
1564 $self->bail_on_events($self->editor->event)
1565 unless $self->editor->update_action_hold_request($hold);
1567 $self->checkin_changed(1);
1569 return 1 if $self->bail_out;
1571 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1573 # This hold was captured in the correct location
1574 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1575 $self->push_events(OpenILS::Event->new('SUCCESS'));
1577 #$self->do_hold_notify($hold->id);
1578 $self->notify_hold($hold->id);
1582 # Hold needs to be picked up elsewhere. Build a hold
1583 # transit and route the item.
1584 $self->checkin_build_hold_transit();
1585 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1586 return 1 if $self->bail_out;
1588 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1591 # make sure we save the copy status
1596 sub do_hold_notify {
1597 my( $self, $holdid ) = @_;
1599 $logger->info("circulator: running delayed hold notify process");
1601 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1602 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1604 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1605 hold_id => $holdid, requestor => $self->editor->requestor);
1607 $logger->debug("circulator: built hold notifier");
1609 if(!$notifier->event) {
1611 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1613 my $stat = $notifier->send_email_notify;
1614 if( $stat == '1' ) {
1615 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1619 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1622 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1627 sub checkin_build_hold_transit {
1630 my $copy = $self->copy;
1631 my $hold = $self->hold;
1632 my $trans = Fieldmapper::action::hold_transit_copy->new;
1634 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1636 $trans->hold($hold->id);
1637 $trans->source($self->editor->requestor->ws_ou);
1638 $trans->dest($hold->pickup_lib);
1639 $trans->source_send_time("now");
1640 $trans->target_copy($copy->id);
1642 # when the copy gets to its destination, it will recover
1643 # this status - put it onto the holds shelf
1644 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1646 return $self->bail_on_events($self->editor->event)
1647 unless $self->editor->create_action_hold_transit_copy($trans);
1652 sub process_received_transit {
1654 my $copy = $self->copy;
1655 my $copyid = $self->copy->id;
1657 my $status_name = $U->copy_status($copy->status)->name;
1658 $logger->debug("circulator: attempting transit receive on ".
1659 "copy $copyid. Copy status is $status_name");
1661 my $transit = $self->transit;
1663 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1664 my $tid = $transit->id;
1665 $logger->info("circulator: Fowarding transit on copy which is destined ".
1666 "for a different location. transit=$tid, copy=$copyid,current ".
1667 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1669 return $self->bail_on_events(
1670 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1673 # The transit is received, set the receive time
1674 $transit->dest_recv_time('now');
1675 $self->bail_on_events($self->editor->event)
1676 unless $self->editor->update_action_transit_copy($transit);
1678 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1680 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1681 $copy->status( $transit->copy_status );
1682 $self->update_copy();
1683 return if $self->bail_out;
1687 #$self->do_hold_notify($hold_transit->hold);
1688 $self->notify_hold($hold_transit->hold);
1693 OpenILS::Event->new(
1696 payload => { transit => $transit, holdtransit => $hold_transit } ));
1698 return $hold_transit;
1702 sub checkin_handle_circ {
1706 my $circ = $self->circ;
1707 my $copy = $self->copy;
1711 # backdate the circ if necessary
1712 if($self->backdate) {
1713 $self->checkin_handle_backdate;
1714 return if $self->bail_out;
1717 if(!$circ->stop_fines) {
1718 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1719 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1720 $circ->stop_fines_time('now') unless $self->backdate;
1721 $circ->stop_fines_time($self->backdate) if $self->backdate;
1724 # see if there are any fines owed on this circ. if not, close it
1725 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1726 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1728 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1730 # Set the checkin vars since we have the item
1731 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1733 $circ->checkin_staff($self->editor->requestor->id);
1734 $circ->checkin_lib($self->editor->requestor->ws_ou);
1736 my $circ_lib = (ref $self->copy->circ_lib) ?
1737 $self->copy->circ_lib->id : $self->copy->circ_lib;
1738 my $stat = $U->copy_status($self->copy->status)->id;
1740 # If the item is lost/missing and it needs to be sent home, don't
1741 # reshelve the copy, leave it lost/missing so the recipient will know
1742 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1743 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1744 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1747 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1751 return $self->bail_on_events($self->editor->event)
1752 unless $self->editor->update_action_circulation($circ);
1756 sub checkin_handle_backdate {
1759 my $bd = $self->backdate;
1761 # ------------------------------------------------------------------
1762 # clean up the backdate for date comparison
1763 # we want any bills created on or after the backdate
1764 # ------------------------------------------------------------------
1765 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1766 #$bd = "${bd}T23:59:59";
1768 my $bills = $self->editor->search_money_billing(
1770 billing_ts => { '>=' => $bd },
1771 xact => $self->circ->id,
1772 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1776 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1778 for my $bill (@$bills) {
1779 unless( $U->is_true($bill->voided) ) {
1780 $logger->info("backdate voiding bill ".$bill->id);
1782 $bill->void_time('now');
1783 $bill->voider($self->editor->requestor->id);
1784 my $n = $bill->note || "";
1785 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1787 $self->bail_on_events($self->editor->event)
1788 unless $self->editor->update_money_billing($bill);
1796 # XXX Legacy version for Circ.pm support
1797 sub _checkin_handle_backdate {
1798 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1801 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1802 $bd = "${bd}T23:59:59";
1804 my $bills = $session->request(
1805 "open-ils.storage.direct.money.billing.search_where.atomic",
1806 billing_ts => { '>=' => $bd },
1808 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1811 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1814 for my $bill (@$bills) {
1815 unless( $U->is_true($bill->voided) ) {
1816 $logger->debug("voiding bill ".$bill->id);
1818 $bill->void_time('now');
1819 $bill->voider($requestor->id);
1820 my $n = $bill->note || "";
1821 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1822 my $s = $session->request(
1823 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1824 return $U->DB_UPDATE_FAILED($bill) unless $s;
1838 sub find_patron_from_copy {
1840 my $circs = $self->editor->search_action_circulation(
1841 { target_copy => $self->copy->id, checkin_time => undef });
1842 my $circ = $circs->[0];
1843 return unless $circ;
1844 my $u = $self->editor->retrieve_actor_user($circ->usr)
1845 or return $self->bail_on_events($self->editor->event);
1849 sub check_checkin_copy_status {
1851 my $copy = $self->copy;
1857 my $status = $U->copy_status($copy->status)->id;
1860 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1861 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1862 $status == OILS_COPY_STATUS_IN_PROCESS ||
1863 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1864 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1865 $status == OILS_COPY_STATUS_CATALOGING ||
1866 $status == OILS_COPY_STATUS_RESHELVING );
1868 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1869 if( $status == OILS_COPY_STATUS_LOST );
1871 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1872 if( $status == OILS_COPY_STATUS_MISSING );
1874 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1879 # --------------------------------------------------------------------------
1880 # On checkin, we need to return as many relevant objects as we can
1881 # --------------------------------------------------------------------------
1882 sub checkin_flesh_events {
1885 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1886 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1887 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1891 for my $evt (@{$self->events}) {
1894 $payload->{copy} = $U->unflesh_copy($self->copy);
1895 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1896 $payload->{circ} = $self->circ;
1897 $payload->{transit} = $self->transit;
1898 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1900 # $self->hold may or may not have been replaced with a
1901 # valid hold after processing a cancelled hold
1902 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1904 $evt->{payload} = $payload;
1909 my( $self, $msg ) = @_;
1910 my $bc = ($self->copy) ? $self->copy->barcode :
1913 my $usr = ($self->patron) ? $self->patron->id : "";
1914 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1915 ", recipient=$usr, copy=$bc");
1921 $self->log_me("do_renew()");
1922 $self->is_renewal(1);
1924 unless( $self->is_renewal ) {
1925 return $self->bail_on_events($self->editor->events)
1926 unless $self->editor->allowed('RENEW_CIRC');
1929 # Make sure there is an open circ to renew that is not
1930 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1931 my $circ = $self->editor->search_action_circulation(
1932 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1935 $circ = $self->editor->search_action_circulation(
1937 target_copy => $self->copy->id,
1938 stop_fines => OILS_STOP_FINES_MAX_FINES,
1939 checkin_time => undef
1944 return $self->bail_on_events($self->editor->event) unless $circ;
1946 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1947 if $circ->renewal_remaining < 1;
1949 # -----------------------------------------------------------------
1951 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1954 $self->run_renew_permit;
1957 $self->do_checkin();
1958 return if $self->bail_out;
1960 unless( $self->permit_override ) {
1962 return if $self->bail_out;
1963 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1964 $self->remove_event('ITEM_NOT_CATALOGED');
1967 $self->override_events;
1968 return if $self->bail_out;
1971 $self->do_checkout();
1976 my( $self, $evt ) = @_;
1977 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1978 $logger->debug("circulator: removing event from list: $evt");
1979 my @events = @{$self->events};
1980 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1985 my( $self, $evt ) = @_;
1986 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1987 return grep { $_->{textcode} eq $evt } @{$self->events};
1992 sub run_renew_permit {
1994 my $runner = $self->script_runner;
1996 $runner->load($self->circ_permit_renew);
1997 my $result = $runner->run or
1998 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1999 my $events = $result->{events};
2001 $logger->activity("ciculator: circ_permit_renew for user ".
2002 $self->patron->id." returned events: @$events") if @$events;
2004 $self->push_events(OpenILS::Event->new($_)) for @$events;
2006 $logger->debug("circulator: re-creating script runner to be safe");
2007 $self->mk_script_runner;