1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenSRF::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
15 my $conf = OpenSRF::Utils::SettingsClient->new;
16 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
17 my @pfx = ( @pfx2, "scripts" );
19 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
20 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
21 my $d = $conf->config_value( @pfx, 'circ_duration' );
22 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
23 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
24 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
25 my $lb = $conf->config_value( @pfx2, 'script_path' );
27 $logger->error( "Missing circ script(s)" )
28 unless( $p and $c and $d and $f and $m and $pr );
30 $scripts{circ_permit_patron} = $p;
31 $scripts{circ_permit_copy} = $c;
32 $scripts{circ_duration} = $d;
33 $scripts{circ_recurring_fines}= $f;
34 $scripts{circ_max_fines} = $m;
35 $scripts{circ_permit_renew} = $pr;
37 $lb = [ $lb ] unless ref($lb);
41 "circulator: Loaded rules scripts for circ: " .
42 "circ permit patron = $p, ".
43 "circ permit copy = $c, ".
44 "circ duration = $d, ".
45 "circ recurring fines = $f, " .
46 "circ max fines = $m, ".
47 "circ renew permit = $pr. ".
52 __PACKAGE__->register_method(
53 method => "run_method",
54 api_name => "open-ils.circ.checkout.permit",
56 Determines if the given checkout can occur
57 @param authtoken The login session key
58 @param params A trailing hash of named params including
59 barcode : The copy barcode,
60 patron : The patron the checkout is occurring for,
61 renew : true or false - whether or not this is a renewal
62 @return The event that occurred during the permit check.
66 __PACKAGE__->register_method (
67 method => 'run_method',
68 api_name => 'open-ils.circ.checkout.permit.override',
69 signature => q/@see open-ils.circ.checkout.permit/,
73 __PACKAGE__->register_method(
74 method => "run_method",
75 api_name => "open-ils.circ.checkout",
78 @param authtoken The login session key
79 @param params A named hash of params including:
81 barcode If no copy is provided, the copy is retrieved via barcode
82 copyid If no copy or barcode is provide, the copy id will be use
83 patron The patron's id
84 noncat True if this is a circulation for a non-cataloted item
85 noncat_type The non-cataloged type id
86 noncat_circ_lib The location for the noncat circ.
87 precat The item has yet to be cataloged
88 dummy_title The temporary title of the pre-cataloded item
89 dummy_author The temporary authr of the pre-cataloded item
90 Default is the home org of the staff member
91 @return The SUCCESS event on success, any other event depending on the error
94 __PACKAGE__->register_method(
95 method => "run_method",
96 api_name => "open-ils.circ.checkin",
99 Generic super-method for handling all copies
100 @param authtoken The login session key
101 @param params Hash of named parameters including:
102 barcode - The copy barcode
103 force - If true, copies in bad statuses will be checked in and give good statuses
108 __PACKAGE__->register_method(
109 method => "run_method",
110 api_name => "open-ils.circ.checkin.override",
111 signature => q/@see open-ils.circ.checkin/
114 __PACKAGE__->register_method(
115 method => "run_method",
116 api_name => "open-ils.circ.renew.override",
117 signature => q/@see open-ils.circ.renew/,
121 __PACKAGE__->register_method(
122 method => "run_method",
123 api_name => "open-ils.circ.renew",
124 notes => <<" NOTES");
125 PARAMS( authtoken, circ => circ_id );
126 open-ils.circ.renew(login_session, circ_object);
127 Renews the provided circulation. login_session is the requestor of the
128 renewal and if the logged in user is not the same as circ->usr, then
129 the logged in user must have RENEW_CIRC permissions.
132 __PACKAGE__->register_method(
133 method => "run_method",
134 api_name => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136 method => "run_method",
137 api_name => "open-ils.circ.checkout.full.override");
142 my( $self, $conn, $auth, $args ) = @_;
143 translate_legacy_args($args);
144 my $api = $self->api_name;
147 OpenILS::Application::Circ::Circulator->new($auth, %$args);
149 return circ_events($circulator) if $circulator->bail_out;
151 # --------------------------------------------------------------------------
152 # Go ahead and load the script runner to make sure we have all
153 # of the objects we need
154 # --------------------------------------------------------------------------
155 $circulator->is_renewal(1) if $api =~ /renew/;
156 $circulator->is_checkin(1) if $api =~ /checkin/;
157 $circulator->mk_script_runner;
158 return circ_events($circulator) if $circulator->bail_out;
160 $circulator->circ_permit_patron($scripts{circ_permit_patron});
161 $circulator->circ_permit_copy($scripts{circ_permit_copy});
162 $circulator->circ_duration($scripts{circ_duration});
163 $circulator->circ_permit_renew($scripts{circ_permit_renew});
165 $circulator->override(1) if $api =~ /override/o;
167 if( $api =~ /checkout\.permit/ ) {
168 $circulator->do_permit();
170 } elsif( $api =~ /checkout.full/ ) {
172 $circulator->do_permit();
173 unless( $circulator->bail_out ) {
174 $circulator->events([]);
175 $circulator->do_checkout();
178 } elsif( $api =~ /checkout/ ) {
179 $circulator->do_checkout();
181 } elsif( $api =~ /checkin/ ) {
182 $circulator->do_checkin();
184 } elsif( $api =~ /renew/ ) {
185 $circulator->is_renewal(1);
186 $circulator->do_renew();
189 if( $circulator->bail_out ) {
192 # make sure no success event accidentally slip in
194 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
195 my @e = @{$circulator->events};
196 push( @ee, $_->{textcode} ) for @e;
197 $logger->info("circulator: bailing out with events: @ee");
198 $circulator->editor->rollback;
201 $circulator->editor->commit;
204 $circulator->script_runner->cleanup;
206 $conn->respond_complete(circ_events($circulator));
208 unless($circulator->bail_out) {
209 $circulator->do_hold_notify($circulator->notify_hold)
210 if $circulator->notify_hold;
216 my @e = @{$circ->events};
217 return (@e == 1) ? $e[0] : \@e;
222 sub translate_legacy_args {
225 if( $$args{barcode} ) {
226 $$args{copy_barcode} = $$args{barcode};
227 delete $$args{barcode};
230 if( $$args{copyid} ) {
231 $$args{copy_id} = $$args{copyid};
232 delete $$args{copyid};
235 if( $$args{patronid} ) {
236 $$args{patron_id} = $$args{patronid};
237 delete $$args{patronid};
240 if( $$args{patron} and !ref($$args{patron}) ) {
241 $$args{patron_id} = $$args{patron};
242 delete $$args{patron};
246 if( $$args{noncat} ) {
247 $$args{is_noncat} = $$args{noncat};
248 delete $$args{noncat};
251 if( $$args{precat} ) {
252 $$args{is_precat} = $$args{precat};
253 delete $$args{precat};
259 # --------------------------------------------------------------------------
260 # This package actually manages all of the circulation logic
261 # --------------------------------------------------------------------------
262 package OpenILS::Application::Circ::Circulator;
263 use strict; use warnings;
264 use vars q/$AUTOLOAD/;
266 use OpenILS::Utils::Fieldmapper;
267 use OpenSRF::Utils::Cache;
268 use Digest::MD5 qw(md5_hex);
269 use DateTime::Format::ISO8601;
270 use OpenILS::Utils::PermitHold;
271 use OpenSRF::Utils qw/:datetime/;
272 use OpenSRF::Utils::SettingsClient;
273 use OpenILS::Application::Circ::Holds;
274 use OpenILS::Application::Circ::Transit;
275 use OpenSRF::Utils::Logger qw(:logger);
276 use OpenILS::Utils::CStoreEditor qw/:funcs/;
277 use OpenILS::Application::Circ::ScriptBuilder;
278 use OpenILS::Const qw/:const/;
280 my $U = "OpenILS::Application::AppUtils";
281 my $holdcode = "OpenILS::Application::Circ::Holds";
282 my $transcode = "OpenILS::Application::Circ::Transit";
287 # --------------------------------------------------------------------------
288 # Add a pile of automagic getter/setter methods
289 # --------------------------------------------------------------------------
290 my @AUTOLOAD_FIELDS = qw/
331 recurring_fines_level
349 my $type = ref($self) or die "$self is not an object";
351 my $name = $AUTOLOAD;
354 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
355 $logger->error("circulator: $type: invalid autoload field: $name");
356 die "$type: invalid autoload field: $name\n"
361 *{"${type}::${name}"} = sub {
364 $s->{$name} = $v if defined $v;
368 return $self->$name($data);
373 my( $class, $auth, %args ) = @_;
374 $class = ref($class) || $class;
375 my $self = bless( {}, $class );
379 new_editor(xact => 1, authtoken => $auth) );
381 unless( $self->editor->checkauth ) {
382 $self->bail_on_events($self->editor->event);
386 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
388 $self->$_($args{$_}) for keys %args;
391 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
397 # --------------------------------------------------------------------------
398 # True if we should discontinue processing
399 # --------------------------------------------------------------------------
401 my( $self, $bool ) = @_;
402 if( defined $bool ) {
403 $logger->info("circulator: BAILING OUT") if $bool;
404 $self->{bail_out} = $bool;
406 return $self->{bail_out};
411 my( $self, @evts ) = @_;
414 $logger->info("circulator: pushing event ".$e->{textcode});
415 push( @{$self->events}, $e ) unless
416 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
422 my $key = md5_hex( time() . rand() . "$$" );
423 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
424 return $self->permit_key($key);
427 sub check_permit_key {
429 my $key = $self->permit_key;
430 return 0 unless $key;
431 my $k = "oils_permit_key_$key";
432 my $one = $self->cache_handle->get_cache($k);
433 $self->cache_handle->delete_cache($k);
434 return ($one) ? 1 : 0;
438 # --------------------------------------------------------------------------
439 # This builds the script runner environment and fetches most of the
441 # --------------------------------------------------------------------------
442 sub mk_script_runner {
448 qw/copy copy_barcode copy_id patron
449 patron_id patron_barcode volume title editor/;
451 # Translate our objects into the ScriptBuilder args hash
452 $$args{$_} = $self->$_() for @fields;
454 $args->{ignore_user_status} = 1 if $self->is_checkin;
455 $$args{fetch_patron_by_circ_copy} = 1;
456 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
458 if( my $pco = $self->pending_checkouts ) {
459 $logger->info("circulator: we were given a pending checkouts number of $pco");
460 $$args{patronItemsOut} = $pco;
463 # This fetches most of the objects we need
464 $self->script_runner(
465 OpenILS::Application::Circ::ScriptBuilder->build($args));
467 # Now we translate the ScriptBuilder objects back into self
468 $self->$_($$args{$_}) for @fields;
470 my @evts = @{$args->{_events}} if $args->{_events};
472 $logger->debug("circulator: script builder returned events: @evts") if @evts;
476 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
477 if(!$self->is_noncat and
479 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
483 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
484 return $self->bail_on_events(@e);
488 $self->is_precat(1) if $self->copy
489 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
491 # We can't renew if there is no copy
492 return $self->bail_on_events(@evts) if
493 $self->is_renewal and !$self->copy;
495 # Set some circ-specific flags in the script environment
496 my $evt = "environment";
497 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
499 if( $self->is_noncat ) {
500 $self->script_runner->insert("$evt.isNonCat", 1);
501 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
504 if( $self->is_precat ) {
505 $self->script_runner->insert("environment.isPrecat", 1, 1);
508 $self->script_runner->add_path( $_ ) for @$script_libs;
516 # --------------------------------------------------------------------------
517 # Does the circ permit work
518 # --------------------------------------------------------------------------
522 $self->log_me("do_permit()");
524 unless( $self->editor->requestor->id == $self->patron->id ) {
525 return $self->bail_on_events($self->editor->event)
526 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
529 $self->check_captured_holds();
530 $self->do_copy_checks();
531 return if $self->bail_out;
532 $self->run_patron_permit_scripts();
533 $self->run_copy_permit_scripts()
534 unless $self->is_precat or $self->is_noncat;
535 $self->override_events() unless $self->is_renewal;
536 return if $self->bail_out;
538 if( $self->is_precat ) {
541 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
542 return $self->bail_out(1) unless $self->is_renewal;
548 payload => $self->mk_permit_key));
552 sub check_captured_holds {
554 my $copy = $self->copy;
555 my $patron = $self->patron;
557 return undef unless $copy;
559 my $s = $U->copy_status($copy->status)->id;
560 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
561 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
563 # Item is on the holds shelf, make sure it's going to the right person
564 my $holds = $self->editor->search_action_hold_request(
567 current_copy => $copy->id ,
568 capture_time => { '!=' => undef },
569 cancel_time => undef,
570 fulfillment_time => undef
576 if( $holds and $$holds[0] ) {
577 return undef if $$holds[0]->usr == $patron->id;
580 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
582 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
588 my $copy = $self->copy;
591 my $stat = $U->copy_status($copy->status)->id;
593 # We cannot check out a copy if it is in-transit
594 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
595 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
598 $self->handle_claims_returned();
599 return if $self->bail_out;
601 # no claims returned circ was found, check if there is any open circ
602 unless( $self->is_renewal ) {
603 my $circs = $self->editor->search_action_circulation(
604 { target_copy => $copy->id, checkin_time => undef }
607 return $self->bail_on_events(
608 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
613 sub send_penalty_request {
615 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
616 $self->penalty_request(
618 'open-ils.penalty.patron_penalty.calculate',
620 authtoken => $self->editor->authtoken,
621 patron => $self->patron } ) );
624 sub gather_penalty_request {
626 return [] unless $self->penalty_request;
627 my $data = $self->penalty_request->recv;
629 throw $data if UNIVERSAL::isa($data,'Error');
630 $data = $data->content;
631 return $data->{fatal_penalties};
633 $logger->error("circulator: penalty request returned no data");
637 # ---------------------------------------------------------------------
638 # This pushes any patron-related events into the list but does not
639 # set bail_out for any events
640 # ---------------------------------------------------------------------
641 sub run_patron_permit_scripts {
643 my $runner = $self->script_runner;
644 my $patronid = $self->patron->id;
646 $self->send_penalty_request() unless $self->is_renewal;
648 # ---------------------------------------------------------------------
649 # Now run the patron permit script
650 # ---------------------------------------------------------------------
651 $runner->load($self->circ_permit_patron);
652 my $result = $runner->run or
653 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
655 my $patron_events = $result->{events};
659 # ---------------------------------------------------------------------
660 # this is policy directly in the code, not a good idea in general, but
661 # the penalty server doesn't know anything about renewals, so we
662 # have to strip the event out here
663 my $penalties = ($self->is_renewal) ? [] : $self->gather_penalty_request();
664 # ---------------------------------------------------------------------
666 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
668 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
670 $self->push_events(@allevents);
674 sub run_copy_permit_scripts {
676 my $copy = $self->copy || return;
677 my $runner = $self->script_runner;
679 # ---------------------------------------------------------------------
680 # Capture all of the copy permit events
681 # ---------------------------------------------------------------------
682 $runner->load($self->circ_permit_copy);
683 my $result = $runner->run or
684 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
685 my $copy_events = $result->{events};
687 # ---------------------------------------------------------------------
688 # Now collect all of the events together
689 # ---------------------------------------------------------------------
691 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
693 # See if this copy has an alert message
694 my $ae = $self->check_copy_alert();
695 push( @allevents, $ae ) if $ae;
697 # uniquify the events
698 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
699 @allevents = values %hash;
702 $_->{payload} = $copy if
703 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
706 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
708 $self->push_events(@allevents);
712 sub check_copy_alert {
714 return undef if $self->is_renewal;
715 return OpenILS::Event->new(
716 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
717 if $self->copy and $self->copy->alert_message;
723 # --------------------------------------------------------------------------
724 # If the call is overriding and has permissions to override every collected
725 # event, the are cleared. Any event that the caller does not have
726 # permission to override, will be left in the event list and bail_out will
728 # XXX We need code in here to cancel any holds/transits on copies
729 # that are being force-checked out
730 # --------------------------------------------------------------------------
731 sub override_events {
733 my @events = @{$self->events};
734 return unless @events;
736 if(!$self->override) {
737 return $self->bail_out(1)
738 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
743 for my $e (@events) {
744 my $tc = $e->{textcode};
745 next if $tc eq 'SUCCESS';
746 my $ov = "$tc.override";
747 $logger->info("circulator: attempting to override event: $ov");
749 return $self->bail_on_events($self->editor->event)
750 unless( $self->editor->allowed($ov) );
755 # --------------------------------------------------------------------------
756 # If there is an open claimsreturn circ on the requested copy, close the
757 # circ if overriding, otherwise bail out
758 # --------------------------------------------------------------------------
759 sub handle_claims_returned {
761 my $copy = $self->copy;
763 my $CR = $self->editor->search_action_circulation(
765 target_copy => $copy->id,
766 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
767 checkin_time => undef,
771 return unless ($CR = $CR->[0]);
775 # - If the caller has set the override flag, we will check the item in
776 if($self->override) {
778 $CR->checkin_time('now');
779 $CR->checkin_lib($self->editor->requestor->ws_ou);
780 $CR->checkin_staff($self->editor->requestor->id);
782 $evt = $self->editor->event
783 unless $self->editor->update_action_circulation($CR);
786 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
789 $self->bail_on_events($evt) if $evt;
794 # --------------------------------------------------------------------------
795 # This performs the checkout
796 # --------------------------------------------------------------------------
800 $self->log_me("do_checkout()");
802 # make sure perms are good if this isn't a renewal
803 unless( $self->is_renewal ) {
804 return $self->bail_on_events($self->editor->event)
805 unless( $self->editor->allowed('COPY_CHECKOUT') );
808 # verify the permit key
809 unless( $self->check_permit_key ) {
810 if( $self->permit_override ) {
811 return $self->bail_on_events($self->editor->event)
812 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
814 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
818 # if this is a non-cataloged circ, build the circ and finish
819 if( $self->is_noncat ) {
820 $self->checkout_noncat;
822 OpenILS::Event->new('SUCCESS',
823 payload => { noncat_circ => $self->circ }));
827 if( $self->is_precat ) {
828 $self->script_runner->insert("environment.isPrecat", 1, 1);
829 $self->make_precat_copy;
830 return if $self->bail_out;
832 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
833 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
836 $self->do_copy_checks;
837 return if $self->bail_out;
839 $self->run_checkout_scripts();
840 return if $self->bail_out;
842 $self->build_checkout_circ_object();
843 return if $self->bail_out;
845 $self->apply_modified_due_date();
846 return if $self->bail_out;
848 return $self->bail_on_events($self->editor->event)
849 unless $self->editor->create_action_circulation($self->circ);
851 # refresh the circ to force local time zone for now
852 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
854 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
856 return if $self->bail_out;
858 $self->handle_checkout_holds();
859 return if $self->bail_out;
861 # ------------------------------------------------------------------------------
862 # Update the patron penalty info in the DB. Run it for permit-overrides or
863 # renewals since both of those cases do not require the penalty server to
864 # run during the permit phase of the checkout
865 # ------------------------------------------------------------------------------
866 if( $self->permit_override or $self->is_renewal ) {
867 $U->update_patron_penalties(
868 authtoken => $self->editor->authtoken,
869 patron => $self->patron,
874 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
876 OpenILS::Event->new('SUCCESS',
878 copy => $U->unflesh_copy($self->copy),
881 holds_fulfilled => $self->fulfilled_holds,
889 my $copy = $self->copy;
891 my $stat = $copy->status if ref $copy->status;
892 my $loc = $copy->location if ref $copy->location;
893 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
895 $copy->status($stat->id) if $stat;
896 $copy->location($loc->id) if $loc;
897 $copy->circ_lib($circ_lib->id) if $circ_lib;
898 $copy->editor($self->editor->requestor->id);
899 $copy->edit_date('now');
900 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
902 return $self->bail_on_events($self->editor->event)
903 unless $self->editor->update_asset_copy($self->copy);
905 $copy->status($U->copy_status($copy->status));
906 $copy->location($loc) if $loc;
907 $copy->circ_lib($circ_lib) if $circ_lib;
912 my( $self, @evts ) = @_;
913 $self->push_events(@evts);
917 sub handle_checkout_holds {
920 my $copy = $self->copy;
921 my $patron = $self->patron;
923 my $holds = $self->editor->search_action_hold_request(
925 current_copy => $copy->id ,
926 cancel_time => undef,
927 fulfillment_time => undef
933 # XXX We should only fulfill one hold here...
934 # XXX If a hold was transited to the user who is checking out
935 # the item, we need to make sure that hold is what's grabbed
938 # for now, just sort by id to get what should be the oldest hold
939 $holds = [ sort { $a->id <=> $b->id } @$holds ];
940 my @myholds = grep { $_->usr eq $patron->id } @$holds;
941 my @altholds = grep { $_->usr ne $patron->id } @$holds;
944 my $hold = $myholds[0];
946 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
948 # if the hold was never officially captured, capture it.
949 $hold->capture_time('now') unless $hold->capture_time;
951 # just make sure it's set correctly
952 $hold->current_copy($copy->id);
954 $hold->fulfillment_time('now');
955 $hold->fulfillment_staff($self->editor->requestor->id);
956 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
958 return $self->bail_on_events($self->editor->event)
959 unless $self->editor->update_action_hold_request($hold);
961 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
963 push( @fulfilled, $hold->id );
966 # If there are any holds placed for other users that point to this copy,
967 # then we need to un-target those holds so the targeter can pick a new copy
970 $logger->info("circulator: un-targeting hold ".$_->id.
971 " because copy ".$copy->id." is getting checked out");
973 # - make the targeter process this hold at next run
974 $_->clear_prev_check_time;
976 # - clear out the targetted copy
977 $_->clear_current_copy;
978 $_->clear_capture_time;
980 return $self->bail_on_event($self->editor->event)
981 unless $self->editor->update_action_hold_request($_);
985 $self->fulfilled_holds(\@fulfilled);
990 sub run_checkout_scripts {
994 my $runner = $self->script_runner;
995 $runner->load($self->circ_duration);
997 my $result = $runner->run or
998 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1000 my $duration = $result->{durationRule};
1001 my $recurring = $result->{recurringFinesRule};
1002 my $max_fine = $result->{maxFine};
1004 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1006 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1007 return $self->bail_on_events($evt) if $evt;
1009 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1010 return $self->bail_on_events($evt) if $evt;
1012 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1013 return $self->bail_on_events($evt) if $evt;
1017 # The item circulates with an unlimited duration
1023 $self->duration_rule($duration);
1024 $self->recurring_fines_rule($recurring);
1025 $self->max_fine_rule($max_fine);
1029 sub build_checkout_circ_object {
1032 my $circ = Fieldmapper::action::circulation->new;
1033 my $duration = $self->duration_rule;
1034 my $max = $self->max_fine_rule;
1035 my $recurring = $self->recurring_fines_rule;
1036 my $copy = $self->copy;
1037 my $patron = $self->patron;
1041 my $dname = $duration->name;
1042 my $mname = $max->name;
1043 my $rname = $recurring->name;
1045 $logger->debug("circulator: building circulation ".
1046 "with duration=$dname, maxfine=$mname, recurring=$rname");
1048 $circ->duration( $duration->shrt )
1049 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1050 $circ->duration( $duration->normal )
1051 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1052 $circ->duration( $duration->extended )
1053 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1055 $circ->recuring_fine( $recurring->low )
1056 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1057 $circ->recuring_fine( $recurring->normal )
1058 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1059 $circ->recuring_fine( $recurring->high )
1060 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1062 $circ->duration_rule( $duration->name );
1063 $circ->recuring_fine_rule( $recurring->name );
1064 $circ->max_fine_rule( $max->name );
1065 $circ->max_fine( $max->amount );
1067 $circ->fine_interval($recurring->recurance_interval);
1068 $circ->renewal_remaining( $duration->max_renewals );
1072 $logger->info("circulator: copy found with an unlimited circ duration");
1073 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1074 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1075 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1076 $circ->renewal_remaining(0);
1079 $circ->target_copy( $copy->id );
1080 $circ->usr( $patron->id );
1081 $circ->circ_lib( $self->circ_lib );
1083 if( $self->is_renewal ) {
1084 $circ->opac_renewal(1);
1085 $circ->renewal_remaining($self->renewal_remaining);
1086 $circ->circ_staff($self->editor->requestor->id);
1089 # if the user provided an overiding checkout time,
1090 # (e.g. the checkout really happened several hours ago), then
1091 # we apply that here. Does this need a perm??
1092 $circ->xact_start(clense_ISO8601($self->checkout_time))
1093 if $self->checkout_time;
1095 # if a patron is renewing, 'requestor' will be the patron
1096 $circ->circ_staff($self->editor->requestor->id);
1097 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1103 sub apply_modified_due_date {
1105 my $circ = $self->circ;
1106 my $copy = $self->copy;
1108 if( $self->due_date ) {
1110 return $self->bail_on_events($self->editor->event)
1111 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1113 $circ->due_date(clense_ISO8601($self->due_date));
1117 # if the due_date lands on a day when the location is closed
1118 return unless $copy and $circ->due_date;
1120 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1122 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1123 " with an item due date of ".$circ->due_date );
1125 my $dateinfo = $U->storagereq(
1126 'open-ils.storage.actor.org_unit.closed_date.overlap',
1127 $org, $circ->due_date );
1130 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1131 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1133 # XXX make the behavior more dynamic
1134 # for now, we just push the due date to after the close date
1135 $circ->due_date($dateinfo->{end});
1142 sub create_due_date {
1143 my( $self, $duration ) = @_;
1144 my ($sec,$min,$hour,$mday,$mon,$year) =
1145 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1146 $year += 1900; $mon += 1;
1147 my $due_date = sprintf(
1148 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1149 $year, $mon, $mday, $hour, $min, $sec);
1155 sub make_precat_copy {
1157 my $copy = $self->copy;
1160 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1162 $copy->editor($self->editor->requestor->id);
1163 $copy->edit_date('now');
1164 $copy->dummy_title($self->dummy_title);
1165 $copy->dummy_author($self->dummy_author);
1167 $self->update_copy();
1171 $logger->info("circulator: Creating a new precataloged ".
1172 "copy in checkout with barcode " . $self->copy_barcode);
1174 $copy = Fieldmapper::asset::copy->new;
1175 $copy->circ_lib($self->circ_lib);
1176 $copy->creator($self->editor->requestor->id);
1177 $copy->editor($self->editor->requestor->id);
1178 $copy->barcode($self->copy_barcode);
1179 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1180 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1181 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1183 $copy->dummy_title($self->dummy_title || "");
1184 $copy->dummy_author($self->dummy_author || "");
1186 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1188 $self->push_events($self->editor->event);
1192 # this is a little bit of a hack, but we need to
1193 # get the copy into the script runner
1194 $self->script_runner->insert("environment.copy", $copy, 1);
1198 sub checkout_noncat {
1204 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1205 my $count = $self->noncat_count || 1;
1206 my $cotime = clense_ISO8601($self->checkout_time) || "";
1208 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1212 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1213 $self->editor->requestor->id,
1221 $self->push_events($evt);
1232 $self->log_me("do_checkin()");
1235 return $self->bail_on_events(
1236 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1239 if( $self->checkin_check_holds_shelf() ) {
1240 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1241 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1242 $self->checkin_flesh_events;
1246 unless( $self->is_renewal ) {
1247 return $self->bail_on_events($self->editor->event)
1248 unless $self->editor->allowed('COPY_CHECKIN');
1251 $self->push_events($self->check_copy_alert());
1252 $self->push_events($self->check_checkin_copy_status());
1254 # the renew code will have already found our circulation object
1255 unless( $self->is_renewal and $self->circ ) {
1257 $self->editor->search_action_circulation(
1258 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1261 # if the circ is marked as 'claims returned', add the event to the list
1262 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1263 if ($self->circ and $self->circ->stop_fines
1264 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1266 # handle the overridable events
1267 $self->override_events unless $self->is_renewal;
1268 return if $self->bail_out;
1272 $self->editor->search_action_transit_copy(
1273 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1277 $self->checkin_handle_circ;
1278 return if $self->bail_out;
1279 $self->checkin_changed(1);
1281 } elsif( $self->transit ) {
1282 my $hold_transit = $self->process_received_transit;
1283 $self->checkin_changed(1);
1285 if( $self->bail_out ) {
1286 $self->checkin_flesh_events;
1290 if( my $e = $self->check_checkin_copy_status() ) {
1291 # If the original copy status is special, alert the caller
1292 my $ev = $self->events;
1293 $self->events([$e]);
1294 $self->override_events;
1295 return if $self->bail_out;
1299 if( $hold_transit or
1300 $U->copy_status($self->copy->status)->id
1301 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1304 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1305 $U->fetch_open_hold_by_copy($self->copy->id)
1308 $self->checkin_flesh_events;
1312 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1313 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1314 " that is in-transit, but there is no transit.. repairing");
1315 $self->reshelve_copy(1);
1316 return if $self->bail_out;
1319 if( $self->is_renewal ) {
1320 $self->push_events(OpenILS::Event->new('SUCCESS'));
1324 # ------------------------------------------------------------------------------
1325 # Circulations and transits are now closed where necessary. Now go on to see if
1326 # this copy can fulfill a hold or needs to be routed to a different location
1327 # ------------------------------------------------------------------------------
1329 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1330 return if $self->bail_out;
1332 } else { # not needed for a hold
1335 my $circ_lib = (ref $self->copy->circ_lib) ?
1336 $self->copy->circ_lib->id : $self->copy->circ_lib;
1338 if( $self->remote_hold ) {
1339 $circ_lib = $self->remote_hold->pickup_lib;
1340 $logger->warn("circulator: Copy ".$self->copy->barcode.
1341 " is on a remote hold's shelf, sending to $circ_lib");
1344 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1346 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1348 $self->checkin_handle_precat();
1349 return if $self->bail_out;
1353 my $bc = $self->copy->barcode;
1354 $logger->info("circulator: copy $bc at a remote lib - sending home");
1355 $self->checkin_build_copy_transit();
1356 return if $self->bail_out;
1357 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1361 $self->reshelve_copy;
1362 return if $self->bail_out;
1364 unless($self->checkin_changed) {
1366 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1367 my $stat = $U->copy_status($self->copy->status)->id;
1369 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1370 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1371 $self->bail_out(1); # no need to commit anything
1374 $self->push_events(OpenILS::Event->new('SUCCESS'))
1375 unless @{$self->events};
1379 # ------------------------------------------------------------------------------
1380 # Update the patron penalty info in the DB
1381 # ------------------------------------------------------------------------------
1382 $U->update_patron_penalties(
1383 authtoken => $self->editor->authtoken,
1384 patron => $self->patron,
1385 background => 1 ) if $self->is_checkin;
1387 $self->checkin_flesh_events;
1393 my $force = $self->force || shift;
1394 my $copy = $self->copy;
1396 my $stat = $U->copy_status($copy->status)->id;
1399 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1400 $stat != OILS_COPY_STATUS_CATALOGING and
1401 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1402 $stat != OILS_COPY_STATUS_RESHELVING )) {
1404 $copy->status( OILS_COPY_STATUS_RESHELVING );
1406 $self->checkin_changed(1);
1411 # Returns true if the item is at the current location
1412 # because it was transited there for a hold and the
1413 # hold has not been fulfilled
1414 sub checkin_check_holds_shelf {
1416 return 0 unless $self->copy;
1419 $U->copy_status($self->copy->status)->id ==
1420 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1422 # find the hold that put us on the holds shelf
1423 my $holds = $self->editor->search_action_hold_request(
1425 current_copy => $self->copy->id,
1426 capture_time => { '!=' => undef },
1427 fulfillment_time => undef,
1428 cancel_time => undef,
1433 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1434 $self->reshelve_copy(1);
1438 my $hold = $$holds[0];
1440 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1441 $hold->id. "] for copy ".$self->copy->barcode);
1443 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1444 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1448 $logger->info("circulator: hold is not for here..");
1449 $self->remote_hold($hold);
1454 sub checkin_handle_precat {
1456 my $copy = $self->copy;
1458 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1459 $copy->status(OILS_COPY_STATUS_CATALOGING);
1460 $self->update_copy();
1461 $self->checkin_changed(1);
1462 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1467 sub checkin_build_copy_transit {
1469 my $copy = $self->copy;
1470 my $transit = Fieldmapper::action::transit_copy->new;
1472 $transit->source($self->editor->requestor->ws_ou);
1473 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1474 $transit->target_copy($copy->id);
1475 $transit->source_send_time('now');
1476 $transit->copy_status( $U->copy_status($copy->status)->id );
1478 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1480 return $self->bail_on_events($self->editor->event)
1481 unless $self->editor->create_action_transit_copy($transit);
1483 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1485 $self->checkin_changed(1);
1489 sub attempt_checkin_hold_capture {
1491 my $copy = $self->copy;
1493 # See if this copy can fulfill any holds
1494 my ($hold) = $holdcode->find_nearest_permitted_hold(
1495 OpenSRF::AppSession->create('open-ils.storage'),
1496 $copy, $self->editor->requestor );
1499 $logger->debug("circulator: no potential permitted".
1500 "holds found for copy ".$copy->barcode);
1505 $logger->info("circulator: found permitted hold ".
1506 $hold->id . " for copy, capturing...");
1508 $hold->current_copy($copy->id);
1509 $hold->capture_time('now');
1511 # prevent DB errors caused by fetching
1512 # holds from storage, and updating through cstore
1513 $hold->clear_fulfillment_time;
1514 $hold->clear_fulfillment_staff;
1515 $hold->clear_fulfillment_lib;
1516 $hold->clear_expire_time;
1517 $hold->clear_cancel_time;
1518 $hold->clear_prev_check_time unless $hold->prev_check_time;
1520 $self->bail_on_events($self->editor->event)
1521 unless $self->editor->update_action_hold_request($hold);
1523 $self->checkin_changed(1);
1525 return 1 if $self->bail_out;
1527 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1529 # This hold was captured in the correct location
1530 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1531 $self->push_events(OpenILS::Event->new('SUCCESS'));
1533 #$self->do_hold_notify($hold->id);
1534 $self->notify_hold($hold->id);
1538 # Hold needs to be picked up elsewhere. Build a hold
1539 # transit and route the item.
1540 $self->checkin_build_hold_transit();
1541 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1542 return 1 if $self->bail_out;
1544 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1547 # make sure we save the copy status
1552 sub do_hold_notify {
1553 my( $self, $holdid ) = @_;
1555 $logger->info("circulator: running delayed hold notify process");
1557 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1558 hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1560 $logger->debug("circulator: built hold notifier");
1562 if(!$notifier->event) {
1564 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1566 my $stat = $notifier->send_email_notify;
1567 if( $stat == '1' ) {
1568 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1572 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1575 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1580 sub checkin_build_hold_transit {
1583 my $copy = $self->copy;
1584 my $hold = $self->hold;
1585 my $trans = Fieldmapper::action::hold_transit_copy->new;
1587 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1589 $trans->hold($hold->id);
1590 $trans->source($self->editor->requestor->ws_ou);
1591 $trans->dest($hold->pickup_lib);
1592 $trans->source_send_time("now");
1593 $trans->target_copy($copy->id);
1595 # when the copy gets to its destination, it will recover
1596 # this status - put it onto the holds shelf
1597 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1599 return $self->bail_on_events($self->editor->event)
1600 unless $self->editor->create_action_hold_transit_copy($trans);
1605 sub process_received_transit {
1607 my $copy = $self->copy;
1608 my $copyid = $self->copy->id;
1610 my $status_name = $U->copy_status($copy->status)->name;
1611 $logger->debug("circulator: attempting transit receive on ".
1612 "copy $copyid. Copy status is $status_name");
1614 my $transit = $self->transit;
1616 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1617 $logger->info("circulator: Fowarding transit on copy which is destined ".
1618 "for a different location. copy=$copyid,current ".
1619 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1621 return $self->bail_on_events(
1622 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1625 # The transit is received, set the receive time
1626 $transit->dest_recv_time('now');
1627 $self->bail_on_events($self->editor->event)
1628 unless $self->editor->update_action_transit_copy($transit);
1630 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1632 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1633 $copy->status( $transit->copy_status );
1634 $self->update_copy();
1635 return if $self->bail_out;
1639 #$self->do_hold_notify($hold_transit->hold);
1640 $self->notify_hold($hold_transit->hold);
1645 OpenILS::Event->new(
1648 payload => { transit => $transit, holdtransit => $hold_transit } ));
1650 return $hold_transit;
1654 sub checkin_handle_circ {
1658 my $circ = $self->circ;
1659 my $copy = $self->copy;
1663 # backdate the circ if necessary
1664 if($self->backdate) {
1665 $self->checkin_handle_backdate;
1666 return if $self->bail_out;
1669 if(!$circ->stop_fines) {
1670 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1671 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1672 $circ->stop_fines_time('now') unless $self->backdate;
1673 $circ->stop_fines_time($self->backdate) if $self->backdate;
1676 # see if there are any fines owed on this circ. if not, close it
1677 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1678 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1680 # Set the checkin vars since we have the item
1681 $circ->checkin_time('now');
1682 $circ->checkin_staff($self->editor->requestor->id);
1683 $circ->checkin_lib($self->editor->requestor->ws_ou);
1685 my $circ_lib = (ref $self->copy->circ_lib) ?
1686 $self->copy->circ_lib->id : $self->copy->circ_lib;
1687 my $stat = $U->copy_status($self->copy->status)->id;
1689 # If the item is lost/missing and it needs to be sent home, don't
1690 # reshelve the copy, leave it lost/missing so the recipient will know
1691 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1692 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1693 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1696 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1701 return $self->bail_on_events($self->editor->event)
1702 unless $self->editor->update_action_circulation($circ);
1706 sub checkin_handle_backdate {
1709 my $bd = $self->backdate;
1710 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1711 $bd = "${bd}T23:59:59";
1713 my $bills = $self->editor->search_money_billing(
1715 billing_ts => { '>=' => $bd },
1716 xact => $self->circ->id,
1717 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1721 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1723 for my $bill (@$bills) {
1724 unless( $U->is_true($bill->voided) ) {
1725 $logger->info("backdate voiding bill ".$bill->id);
1727 $bill->void_time('now');
1728 $bill->voider($self->editor->requestor->id);
1729 my $n = $bill->note || "";
1730 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1732 $self->bail_on_events($self->editor->event)
1733 unless $self->editor->update_money_billing($bill);
1741 # XXX Legacy version for Circ.pm support
1742 sub _checkin_handle_backdate {
1743 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1746 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1747 $bd = "${bd}T23:59:59";
1749 my $bills = $session->request(
1750 "open-ils.storage.direct.money.billing.search_where.atomic",
1751 billing_ts => { '>=' => $bd },
1753 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1756 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1759 for my $bill (@$bills) {
1760 unless( $U->is_true($bill->voided) ) {
1761 $logger->debug("voiding bill ".$bill->id);
1763 $bill->void_time('now');
1764 $bill->voider($requestor->id);
1765 my $n = $bill->note || "";
1766 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1767 my $s = $session->request(
1768 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1769 return $U->DB_UPDATE_FAILED($bill) unless $s;
1783 sub find_patron_from_copy {
1785 my $circs = $self->editor->search_action_circulation(
1786 { target_copy => $self->copy->id, checkin_time => undef });
1787 my $circ = $circs->[0];
1788 return unless $circ;
1789 my $u = $self->editor->retrieve_actor_user($circ->usr)
1790 or return $self->bail_on_events($self->editor->event);
1794 sub check_checkin_copy_status {
1796 my $copy = $self->copy;
1802 my $status = $U->copy_status($copy->status)->id;
1805 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1806 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1807 $status == OILS_COPY_STATUS_IN_PROCESS ||
1808 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1809 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1810 $status == OILS_COPY_STATUS_CATALOGING ||
1811 $status == OILS_COPY_STATUS_RESHELVING );
1813 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1814 if( $status == OILS_COPY_STATUS_LOST );
1816 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1817 if( $status == OILS_COPY_STATUS_MISSING );
1819 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1824 # --------------------------------------------------------------------------
1825 # On checkin, we need to return as many relevant objects as we can
1826 # --------------------------------------------------------------------------
1827 sub checkin_flesh_events {
1830 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1831 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1832 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1836 for my $evt (@{$self->events}) {
1839 $payload->{copy} = $U->unflesh_copy($self->copy);
1840 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1841 $payload->{circ} = $self->circ;
1842 $payload->{transit} = $self->transit;
1843 $payload->{hold} = $self->hold;
1845 $evt->{payload} = $payload;
1850 my( $self, $msg ) = @_;
1851 my $bc = ($self->copy) ? $self->copy->barcode :
1854 my $usr = ($self->patron) ? $self->patron->id : "";
1855 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1856 ", recipient=$usr, copy=$bc");
1862 $self->log_me("do_renew()");
1863 $self->is_renewal(1);
1865 unless( $self->is_renewal ) {
1866 return $self->bail_on_events($self->editor->events)
1867 unless $self->editor->allowed('RENEW_CIRC');
1870 # Make sure there is an open circ to renew that is not
1871 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1872 my $circ = $self->editor->search_action_circulation(
1873 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1876 $circ = $self->editor->search_action_circulation(
1878 target_copy => $self->copy->id,
1879 stop_fines => OILS_STOP_FINES_MAX_FINES,
1880 checkin_time => undef
1885 return $self->bail_on_events($self->editor->event) unless $circ;
1887 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1888 if $circ->renewal_remaining < 1;
1890 # -----------------------------------------------------------------
1892 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1895 $self->run_renew_permit;
1898 $self->do_checkin();
1899 return if $self->bail_out;
1901 unless( $self->permit_override ) {
1903 return if $self->bail_out;
1904 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1905 $self->remove_event('ITEM_NOT_CATALOGED');
1908 $self->override_events;
1909 return if $self->bail_out;
1912 $self->do_checkout();
1917 my( $self, $evt ) = @_;
1918 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1919 $logger->debug("circulator: removing event from list: $evt");
1920 my @events = @{$self->events};
1921 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1926 my( $self, $evt ) = @_;
1927 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1928 return grep { $_->{textcode} eq $evt } @{$self->events};
1933 sub run_renew_permit {
1935 my $runner = $self->script_runner;
1937 $runner->load($self->circ_permit_renew);
1938 my $result = $runner->run or
1939 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1940 my $events = $result->{events};
1942 $logger->activity("ciculator: circ_permit_renew for user ".
1943 $self->patron->id." returned events: @$events") if @$events;
1945 $self->push_events(OpenILS::Event->new($_)) for @$events;
1947 $logger->debug("circulator: re-creating script runner to be safe");
1948 $self->mk_script_runner;