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 "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.
134 my( $self, $conn, $auth, $args ) = @_;
135 translate_legacy_args($args);
136 my $api = $self->api_name;
139 OpenILS::Application::Circ::Circulator->new($auth, %$args);
141 return circ_events($circulator) if $circulator->bail_out;
143 # --------------------------------------------------------------------------
144 # Go ahead and load the script runner to make sure we have all
145 # of the objects we need
146 # --------------------------------------------------------------------------
147 $circulator->is_renewal(1) if $api =~ /renew/;
148 $circulator->is_checkin(1) if $api =~ /checkin/;
149 $circulator->mk_script_runner;
150 return circ_events($circulator) if $circulator->bail_out;
152 $circulator->circ_permit_patron($scripts{circ_permit_patron});
153 $circulator->circ_permit_copy($scripts{circ_permit_copy});
154 $circulator->circ_duration($scripts{circ_duration});
155 $circulator->circ_permit_renew($scripts{circ_permit_renew});
157 $circulator->override(1) if $api =~ /override/o;
159 if( $api =~ /checkout\.permit/ ) {
160 $circulator->do_permit();
162 } elsif( $api =~ /checkout/ ) {
163 $circulator->do_checkout();
165 } elsif( $api =~ /checkin/ ) {
166 $circulator->do_checkin();
168 } elsif( $api =~ /renew/ ) {
169 $circulator->is_renewal(1);
170 $circulator->do_renew();
173 if( $circulator->bail_out ) {
176 # make sure no success event accidentally slip in
178 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
179 my @e = @{$circulator->events};
180 push( @ee, $_->{textcode} ) for @e;
181 $logger->info("circulator: bailing out with events: @ee");
182 $circulator->editor->xact_rollback;
185 $circulator->editor->commit;
188 $circulator->script_runner->cleanup;
190 return circ_events($circulator);
195 my @e = @{$circ->events};
196 return (@e == 1) ? $e[0] : \@e;
201 sub translate_legacy_args {
204 if( $$args{barcode} ) {
205 $$args{copy_barcode} = $$args{barcode};
206 delete $$args{barcode};
209 if( $$args{copyid} ) {
210 $$args{copy_id} = $$args{copyid};
211 delete $$args{copyid};
214 if( $$args{patronid} ) {
215 $$args{patron_id} = $$args{patronid};
216 delete $$args{patronid};
219 if( $$args{patron} and !ref($$args{patron}) ) {
220 $$args{patron_id} = $$args{patron};
221 delete $$args{patron};
225 if( $$args{noncat} ) {
226 $$args{is_noncat} = $$args{noncat};
227 delete $$args{noncat};
230 if( $$args{precat} ) {
231 $$args{is_precat} = $$args{precat};
232 delete $$args{precat};
238 # --------------------------------------------------------------------------
239 # This package actually manages all of the circulation logic
240 # --------------------------------------------------------------------------
241 package OpenILS::Application::Circ::Circulator;
242 use strict; use warnings;
243 use vars q/$AUTOLOAD/;
245 use OpenILS::Utils::Fieldmapper;
246 use OpenSRF::Utils::Cache;
247 use Digest::MD5 qw(md5_hex);
248 use DateTime::Format::ISO8601;
249 use OpenILS::Utils::PermitHold;
250 use OpenSRF::Utils qw/:datetime/;
251 use OpenSRF::Utils::SettingsClient;
252 use OpenILS::Application::Circ::Holds;
253 use OpenILS::Application::Circ::Transit;
254 use OpenSRF::Utils::Logger qw(:logger);
255 use OpenILS::Utils::CStoreEditor qw/:funcs/;
256 use OpenILS::Application::Circ::ScriptBuilder;
257 use OpenILS::Const qw/:const/;
259 my $U = "OpenILS::Application::AppUtils";
260 my $holdcode = "OpenILS::Application::Circ::Holds";
261 my $transcode = "OpenILS::Application::Circ::Transit";
266 # --------------------------------------------------------------------------
267 # Add a pile of automagic getter/setter methods
268 # --------------------------------------------------------------------------
269 my @AUTOLOAD_FIELDS = qw/
307 recurring_fines_level
324 my $type = ref($self) or die "$self is not an object";
326 my $name = $AUTOLOAD;
329 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
330 $logger->error("$type: invalid autoload field: $name");
331 die "$type: invalid autoload field: $name\n"
336 *{"${type}::${name}"} = sub {
339 $s->{$name} = $v if defined $v;
343 return $self->$name($data);
348 my( $class, $auth, %args ) = @_;
349 $class = ref($class) || $class;
350 my $self = bless( {}, $class );
354 new_editor(xact => 1, authtoken => $auth) );
356 unless( $self->editor->checkauth ) {
357 $self->bail_on_events($self->editor->event);
361 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
363 $self->$_($args{$_}) for keys %args;
366 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
372 # --------------------------------------------------------------------------
373 # True if we should discontinue processing
374 # --------------------------------------------------------------------------
376 my( $self, $bool ) = @_;
377 if( defined $bool ) {
378 $logger->info("circulator: BAILING OUT") if $bool;
379 $self->{bail_out} = $bool;
381 return $self->{bail_out};
386 my( $self, @evts ) = @_;
389 $logger->info("circulator: pushing event ".$e->{textcode});
390 push( @{$self->events}, $e ) unless
391 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
397 my $key = md5_hex( time() . rand() . "$$" );
398 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
399 return $self->permit_key($key);
402 sub check_permit_key {
404 my $key = $self->permit_key;
405 return 0 unless $key;
406 my $k = "oils_permit_key_$key";
407 my $one = $self->cache_handle->get_cache($k);
408 $self->cache_handle->delete_cache($k);
409 return ($one) ? 1 : 0;
413 # --------------------------------------------------------------------------
414 # This builds the script runner environment and fetches most of the
416 # --------------------------------------------------------------------------
417 sub mk_script_runner {
421 $args->{ignore_user_status} = 1 if $self->is_checkin;
424 qw/copy copy_barcode copy_id patron
425 patron_id patron_barcode volume title editor/;
427 # Translate our objects into the ScriptBuilder args hash
428 $$args{$_} = $self->$_() for @fields;
429 $$args{fetch_patron_by_circ_copy} = 1;
430 $$args{fetch_patron_circ_info} = 1;
432 # This fetches most of the objects we need
433 $self->script_runner(
434 OpenILS::Application::Circ::ScriptBuilder->build($args));
436 # Now we translate the ScriptBuilder objects back into self
437 $self->$_($$args{$_}) for @fields;
439 my @evts = @{$args->{_events}} if $args->{_events};
441 $logger->debug("script builder returned events: : @evts") if @evts;
445 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
446 if(!$self->is_noncat and
448 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
452 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
453 return $self->bail_on_events(@e);
457 $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
459 # Set some circ-specific flags in the script environment
460 my $evt = "environment";
461 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
463 if( $self->is_noncat ) {
464 $self->script_runner->insert("$evt.isNonCat", 1);
465 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
468 if( $self->is_precat ) {
469 $self->script_runner->insert("environment.isPrecat", 1, 1);
472 $self->script_runner->add_path( $_ ) for @$script_libs;
480 # --------------------------------------------------------------------------
481 # Does the circ permit work
482 # --------------------------------------------------------------------------
486 $self->log_me("do_permit()");
488 unless( $self->editor->requestor->id == $self->patron->id ) {
489 return $self->bail_on_events($self->editor->event)
490 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
493 $self->do_copy_checks();
494 return if $self->bail_out;
495 $self->run_patron_permit_scripts();
496 $self->run_copy_permit_scripts()
497 unless $self->is_precat or $self->is_noncat;
498 $self->override_events() unless $self->is_renewal;
499 return if $self->bail_out;
501 if( $self->is_precat ) {
504 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
505 return $self->bail_out(1) unless $self->is_renewal;
511 payload => $self->mk_permit_key));
517 my $copy = $self->copy;
520 my $stat = $U->copy_status($copy->status)->id;
522 # We cannot check out a copy if it is in-transit
523 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
524 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
527 $self->handle_claims_returned();
528 return if $self->bail_out;
530 # no claims returned circ was found, check if there is any open circ
531 unless( $self->is_renewal ) {
532 my $circs = $self->editor->search_action_circulation(
533 { target_copy => $copy->id, checkin_time => undef }
536 return $self->bail_on_events(
537 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
542 # ---------------------------------------------------------------------
543 # This pushes any patron-related events into the list but does not
544 # set bail_out for any events
545 # ---------------------------------------------------------------------
546 sub run_patron_permit_scripts {
548 my $runner = $self->script_runner;
549 my $patronid = $self->patron->id;
551 # ---------------------------------------------------------------------
552 # Find all of the fatal penalties currently set on the user
553 # ---------------------------------------------------------------------
554 my $penalties = $U->update_patron_penalties(
555 authtoken => $self->editor->authtoken,
556 patron => $self->patron,
559 $penalties = $penalties->{fatal_penalties};
562 # ---------------------------------------------------------------------
563 # Now run the patron permit script
564 # ---------------------------------------------------------------------
565 $runner->load($self->circ_permit_patron);
566 my $result = $runner->run or
567 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
569 my $patron_events = $result->{events};
571 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
573 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
575 $self->push_events(@allevents);
579 sub run_copy_permit_scripts {
581 my $copy = $self->copy || return;
582 my $runner = $self->script_runner;
584 # ---------------------------------------------------------------------
585 # Capture all of the copy permit events
586 # ---------------------------------------------------------------------
587 $runner->load($self->circ_permit_copy);
588 my $result = $runner->run or
589 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
590 my $copy_events = $result->{events};
592 # ---------------------------------------------------------------------
593 # Now collect all of the events together
594 # ---------------------------------------------------------------------
596 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
598 # See if this copy has an alert message
599 my $ae = $self->check_copy_alert();
600 push( @allevents, $ae ) if $ae;
602 # uniquify the events
603 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
604 @allevents = values %hash;
607 $_->{payload} = $copy if
608 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
611 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
613 $self->push_events(@allevents);
617 sub check_copy_alert {
619 return OpenILS::Event->new(
620 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
621 if $self->copy and $self->copy->alert_message;
627 # --------------------------------------------------------------------------
628 # If the call is overriding and has permissions to override every collected
629 # event, the are cleared. Any event that the caller does not have
630 # permission to override, will be left in the event list and bail_out will
632 # XXX We need code in here to cancel any holds/transits on copies
633 # that are being force-checked out
634 # --------------------------------------------------------------------------
635 sub override_events {
637 my @events = @{$self->events};
638 return unless @events;
640 if(!$self->override) {
641 return $self->bail_out(1)
642 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
647 for my $e (@events) {
648 my $tc = $e->{textcode};
649 next if $tc eq 'SUCCESS';
650 my $ov = "$tc.override";
651 $logger->info("circulator: attempting to override event: $ov");
653 return $self->bail_on_events($self->editor->event)
654 unless( $self->editor->allowed($ov) );
659 # --------------------------------------------------------------------------
660 # If there is an open claimsreturn circ on the requested copy, close the
661 # circ if overriding, otherwise bail out
662 # --------------------------------------------------------------------------
663 sub handle_claims_returned {
665 my $copy = $self->copy;
667 my $CR = $self->editor->search_action_circulation(
669 target_copy => $copy->id,
670 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
671 checkin_time => undef,
675 return unless ($CR = $CR->[0]);
679 # - If the caller has set the override flag, we will check the item in
680 if($self->override) {
682 $CR->checkin_time('now');
683 $CR->checkin_lib($self->editor->requestor->ws_ou);
684 $CR->checkin_staff($self->editor->requestor->id);
686 $evt = $self->editor->event
687 unless $self->editor->update_action_circulation($CR);
690 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
693 $self->bail_on_events($evt) if $evt;
698 # --------------------------------------------------------------------------
699 # This performs the checkout
700 # --------------------------------------------------------------------------
704 $self->log_me("do_checkout()");
706 # make sure perms are good if this isn't a renewal
707 unless( $self->is_renewal ) {
708 return $self->bail_on_events($self->editor->event)
709 unless( $self->editor->allowed('COPY_CHECKOUT') );
712 # verify the permit key
713 unless( $self->check_permit_key ) {
714 if( $self->permit_override ) {
715 return $self->bail_on_events($self->editor->event)
716 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
718 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
722 # if this is a non-cataloged circ, build the circ and finish
723 if( $self->is_noncat ) {
724 $self->checkout_noncat;
726 OpenILS::Event->new('SUCCESS',
727 payload => { noncat_circ => $self->circ }));
731 if( $self->is_precat ) {
732 $self->script_runner->insert("environment.isPrecat", 1, 1);
733 $self->make_precat_copy;
734 return if $self->bail_out;
736 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
737 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
740 $self->do_copy_checks;
741 return if $self->bail_out;
743 $self->run_checkout_scripts();
744 return if $self->bail_out;
746 $self->build_checkout_circ_object();
747 return if $self->bail_out;
749 $self->apply_modified_due_date();
750 return if $self->bail_out;
752 return $self->bail_on_events($self->editor->event)
753 unless $self->editor->create_action_circulation($self->circ);
755 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
757 return if $self->bail_out;
759 $self->handle_checkout_holds();
760 return if $self->bail_out;
762 # ------------------------------------------------------------------------------
763 # Update the patron penalty info in the DB
764 # ------------------------------------------------------------------------------
765 $U->update_patron_penalties(
766 authtoken => $self->editor->authtoken,
767 patron => $self->patron,
771 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
773 OpenILS::Event->new('SUCCESS',
775 copy => $U->unflesh_copy($self->copy),
778 holds_fulfilled => $self->fulfilled_holds,
786 my $copy = $self->copy;
788 my $stat = $copy->status if ref $copy->status;
789 my $loc = $copy->location if ref $copy->location;
790 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
792 $copy->status($stat->id) if $stat;
793 $copy->location($loc->id) if $loc;
794 $copy->circ_lib($circ_lib->id) if $circ_lib;
795 $copy->editor($self->editor->requestor->id);
796 $copy->edit_date('now');
798 return $self->bail_on_events($self->editor->event)
799 unless $self->editor->update_asset_copy($self->copy);
801 $copy->status($U->copy_status($copy->status));
802 $copy->location($loc) if $loc;
803 $copy->circ_lib($circ_lib) if $circ_lib;
808 my( $self, @evts ) = @_;
809 $self->push_events(@evts);
813 sub handle_checkout_holds {
816 my $copy = $self->copy;
817 my $patron = $self->patron;
819 my $holds = $self->editor->search_action_hold_request(
821 current_copy => $copy->id ,
822 cancel_time => undef,
823 fulfillment_time => undef
829 # XXX We should only fulfill one hold here...
830 # XXX If a hold was transited to the user who is checking out
831 # the item, we need to make sure that hold is what's grabbed
834 # for now, just sort by id to get what should be the oldest hold
835 $holds = [ sort { $a->id <=> $b->id } @$holds ];
836 my @myholds = grep { $_->usr eq $patron->id } @$holds;
837 my @altholds = grep { $_->usr ne $patron->id } @$holds;
840 my $hold = $myholds[0];
842 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
844 # if the hold was never officially captured, capture it.
845 $hold->capture_time('now') unless $hold->capture_time;
847 # just make sure it's set correctly
848 $hold->current_copy($copy->id);
850 $hold->fulfillment_time('now');
851 $hold->fulfillment_staff($self->editor->requestor->id);
852 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
854 return $self->bail_on_events($self->editor->event)
855 unless $self->editor->update_action_hold_request($hold);
857 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
859 push( @fulfilled, $hold->id );
862 # If there are any holds placed for other users that point to this copy,
863 # then we need to un-target those holds so the targeter can pick a new copy
866 $logger->info("circulator: un-targeting hold ".$_->id.
867 " because copy ".$copy->id." is getting checked out");
869 # - make the targeter process this hold at next run
870 $_->clear_prev_check_time;
872 # - clear out the targetted copy
873 $_->clear_current_copy;
874 $_->clear_capture_time;
876 return $self->bail_on_event($self->editor->event)
877 unless $self->editor->update_action_hold_request($_);
881 $self->fulfilled_holds(\@fulfilled);
886 sub run_checkout_scripts {
890 my $runner = $self->script_runner;
891 $runner->load($self->circ_duration);
893 my $result = $runner->run or
894 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
896 my $duration = $result->{durationRule};
897 my $recurring = $result->{recurringFinesRule};
898 my $max_fine = $result->{maxFine};
900 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
902 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
903 return $self->bail_on_events($evt) if $evt;
905 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
906 return $self->bail_on_events($evt) if $evt;
908 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
909 return $self->bail_on_events($evt) if $evt;
913 # The item circulates with an unlimited duration
919 $self->duration_rule($duration);
920 $self->recurring_fines_rule($recurring);
921 $self->max_fine_rule($max_fine);
925 sub build_checkout_circ_object {
928 my $circ = Fieldmapper::action::circulation->new;
929 my $duration = $self->duration_rule;
930 my $max = $self->max_fine_rule;
931 my $recurring = $self->recurring_fines_rule;
932 my $copy = $self->copy;
933 my $patron = $self->patron;
937 my $dname = $duration->name;
938 my $mname = $max->name;
939 my $rname = $recurring->name;
941 $logger->debug("circulator: building circulation ".
942 "with duration=$dname, maxfine=$mname, recurring=$rname");
944 $circ->duration( $duration->shrt )
945 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
946 $circ->duration( $duration->normal )
947 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
948 $circ->duration( $duration->extended )
949 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
951 $circ->recuring_fine( $recurring->low )
952 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
953 $circ->recuring_fine( $recurring->normal )
954 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
955 $circ->recuring_fine( $recurring->high )
956 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
958 $circ->duration_rule( $duration->name );
959 $circ->recuring_fine_rule( $recurring->name );
960 $circ->max_fine_rule( $max->name );
961 $circ->max_fine( $max->amount );
963 $circ->fine_interval($recurring->recurance_interval);
964 $circ->renewal_remaining( $duration->max_renewals );
968 $logger->info("circulator: copy found with an unlimited circ duration");
969 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
970 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
971 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
972 $circ->renewal_remaining(0);
975 $circ->target_copy( $copy->id );
976 $circ->usr( $patron->id );
977 $circ->circ_lib( $self->circ_lib );
979 if( $self->is_renewal ) {
980 $circ->opac_renewal(1);
981 $circ->renewal_remaining($self->renewal_remaining);
982 $circ->circ_staff($self->editor->requestor->id);
985 # if the user provided an overiding checkout time,
986 # (e.g. the checkout really happened several hours ago), then
987 # we apply that here. Does this need a perm??
988 $circ->xact_start(clense_ISO8601($self->checkout_time))
989 if $self->checkout_time;
991 # if a patron is renewing, 'requestor' will be the patron
992 $circ->circ_staff($self->editor->requestor->id);
993 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
999 sub apply_modified_due_date {
1001 my $circ = $self->circ;
1002 my $copy = $self->copy;
1004 if( $self->due_date ) {
1006 return $self->bail_on_events($self->editor->event)
1007 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1009 $circ->due_date(clense_ISO8601($self->due_date));
1013 # if the due_date lands on a day when the location is closed
1014 return unless $copy and $circ->due_date;
1016 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1018 $logger->info("circ searching for closed date overlap on lib $org".
1019 " with an item due date of ".$circ->due_date );
1021 my $dateinfo = $U->storagereq(
1022 'open-ils.storage.actor.org_unit.closed_date.overlap',
1023 $org, $circ->due_date );
1026 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1027 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1029 # XXX make the behavior more dynamic
1030 # for now, we just push the due date to after the close date
1031 $circ->due_date($dateinfo->{end});
1038 sub create_due_date {
1039 my( $self, $duration ) = @_;
1040 my ($sec,$min,$hour,$mday,$mon,$year) =
1041 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1042 $year += 1900; $mon += 1;
1043 my $due_date = sprintf(
1044 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1045 $year, $mon, $mday, $hour, $min, $sec);
1051 sub make_precat_copy {
1053 my $copy = $self->copy;
1056 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1058 $copy->editor($self->editor->requestor->id);
1059 $copy->edit_date('now');
1060 $copy->dummy_title($self->dummy_title);
1061 $copy->dummy_author($self->dummy_author);
1063 $self->update_copy();
1067 $logger->info("circulator: Creating a new precataloged ".
1068 "copy in checkout with barcode " . $self->copy_barcode);
1070 $copy = Fieldmapper::asset::copy->new;
1071 $copy->circ_lib($self->circ_lib);
1072 $copy->creator($self->editor->requestor->id);
1073 $copy->editor($self->editor->requestor->id);
1074 $copy->barcode($self->copy_barcode);
1075 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1076 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1077 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1079 $copy->dummy_title($self->dummy_title || "");
1080 $copy->dummy_author($self->dummy_author || "");
1082 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1084 $self->push_events($self->editor->event);
1088 # this is a little bit of a hack, but we need to
1089 # get the copy into the script runner
1090 $self->script_runner->insert("environment.copy", $copy, 1);
1094 sub checkout_noncat {
1100 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1101 my $count = $self->noncat_count || 1;
1102 my $cotime = clense_ISO8601($self->checkout_time) || "";
1104 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1108 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1109 $self->editor->requestor->id,
1117 $self->push_events($evt);
1128 $self->log_me("do_checkin()");
1130 return $self->bail_on_events(
1131 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1134 unless( $self->is_renewal ) {
1135 return $self->bail_on_events($self->editor->event)
1136 unless $self->editor->allowed('COPY_CHECKIN');
1139 $self->push_events($self->check_copy_alert());
1140 $self->push_events($self->check_checkin_copy_status());
1142 # the renew code will have already found our circulation object
1143 unless( $self->is_renewal and $self->circ ) {
1145 $self->editor->search_action_circulation(
1146 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1149 # if the circ is marked as 'claims returned', add the event to the list
1150 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1151 if ($self->circ and $self->circ->stop_fines
1152 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1154 # handle the overridable events
1155 $self->override_events unless $self->is_renewal;
1156 return if $self->bail_out;
1160 $self->editor->search_action_transit_copy(
1161 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1165 $self->checkin_handle_circ;
1166 return if $self->bail_out;
1167 $self->checkin_changed(1);
1169 } elsif( $self->transit ) {
1170 my $hold_transit = $self->process_received_transit;
1171 $self->checkin_changed(1);
1173 if( $self->bail_out ) {
1174 $self->checkin_flesh_events;
1178 if( my $e = $self->check_checkin_copy_status() ) {
1179 # If the original copy status is special, alert the caller
1180 my $ev = $self->events;
1181 $self->events([$e]);
1182 $self->override_events;
1183 return if $self->bail_out;
1188 if( $hold_transit or
1189 $U->copy_status($self->copy->status)->id
1190 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1191 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1192 $self->checkin_flesh_events;
1197 if( $self->is_renewal ) {
1198 $self->push_events(OpenILS::Event->new('SUCCESS'));
1202 # ------------------------------------------------------------------------------
1203 # Circulations and transits are now closed where necessary. Now go on to see if
1204 # this copy can fulfill a hold or needs to be routed to a different location
1205 # ------------------------------------------------------------------------------
1207 if( $self->attempt_checkin_hold_capture() ) {
1208 return if $self->bail_out;
1210 } else { # not needed for a hold
1212 my $circ_lib = (ref $self->copy->circ_lib) ?
1213 $self->copy->circ_lib->id : $self->copy->circ_lib;
1215 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1217 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1219 $self->checkin_handle_precat();
1220 return if $self->bail_out;
1224 $self->checkin_build_copy_transit();
1225 return if $self->bail_out;
1226 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1231 $self->reshelve_copy;
1232 return if $self->bail_out;
1234 unless($self->checkin_changed) {
1236 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1237 my $stat = $U->copy_status($self->copy->status)->id;
1239 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1240 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1241 $self->bail_out(1); # no need to commit anything
1244 $self->push_events(OpenILS::Event->new('SUCCESS'))
1245 unless @{$self->events};
1248 $self->checkin_flesh_events;
1254 my $force = $self->force || shift;
1255 my $copy = $self->copy;
1257 my $stat = $U->copy_status($copy->status)->id;
1260 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1261 $stat != OILS_COPY_STATUS_CATALOGING and
1262 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1263 $stat != OILS_COPY_STATUS_RESHELVING )) {
1265 $copy->status( OILS_COPY_STATUS_RESHELVING );
1267 $self->checkin_changed(1);
1272 sub checkin_handle_precat {
1274 my $copy = $self->copy;
1276 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1277 $copy->status(OILS_COPY_STATUS_CATALOGING);
1278 $self->update_copy();
1279 $self->checkin_changed(1);
1280 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1285 sub checkin_build_copy_transit {
1287 my $copy = $self->copy;
1288 my $transit = Fieldmapper::action::transit_copy->new;
1290 $transit->source($self->editor->requestor->ws_ou);
1291 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1292 $transit->target_copy($copy->id);
1293 $transit->source_send_time('now');
1294 $transit->copy_status( $U->copy_status($copy->status)->id );
1296 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1298 return $self->bail_on_events($self->editor->event)
1299 unless $self->editor->create_action_transit_copy($transit);
1301 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1303 $self->checkin_changed(1);
1307 sub attempt_checkin_hold_capture {
1309 my $copy = $self->copy;
1311 # See if this copy can fulfill any holds
1312 my ($hold) = $holdcode->find_nearest_permitted_hold(
1313 OpenSRF::AppSession->create('open-ils.storage'),
1314 $copy, $self->editor->requestor );
1317 $logger->debug("circulator: no potential permitted".
1318 "holds found for copy ".$copy->barcode);
1323 $logger->info("circulator: found permitted hold ".
1324 $hold->id . " for copy, capturing...");
1326 $hold->current_copy($copy->id);
1327 $hold->capture_time('now');
1329 # prevent DB errors caused by fetching
1330 # holds from storage, and updating through cstore
1331 $hold->clear_fulfillment_time;
1332 $hold->clear_fulfillment_staff;
1333 $hold->clear_fulfillment_lib;
1334 $hold->clear_expire_time;
1335 $hold->clear_cancel_time;
1336 $hold->clear_prev_check_time unless $hold->prev_check_time;
1338 $self->bail_on_events($self->editor->event)
1339 unless $self->editor->update_action_hold_request($hold);
1341 $self->checkin_changed(1);
1343 return 1 if $self->bail_out;
1345 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1347 # This hold was captured in the correct location
1348 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1349 $self->push_events(OpenILS::Event->new('SUCCESS'));
1351 $self->do_hold_notify($hold->id);
1355 # Hold needs to be picked up elsewhere. Build a hold
1356 # transit and route the item.
1357 $self->checkin_build_hold_transit();
1358 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1359 return 1 if $self->bail_out;
1361 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1364 # make sure we save the copy status
1369 sub do_hold_notify {
1370 my( $self, $holdid ) = @_;
1371 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1372 editor => $self->editor, hold_id => $holdid );
1374 if(!$notifier->event) {
1376 $logger->info("attempt at sending hold notification for hold $holdid");
1378 my $stat = $notifier->send_email_notify;
1379 $logger->info("hold notify succeeded for hold $holdid") if $stat eq '1';
1380 $logger->warn(" * hold notify failed for hold $holdid") if $stat ne '1';
1383 $logger->info("Not sending hold notification since the patron has no email address");
1388 sub checkin_build_hold_transit {
1392 my $copy = $self->copy;
1393 my $hold = $self->hold;
1394 my $trans = Fieldmapper::action::hold_transit_copy->new;
1396 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1398 $trans->hold($hold->id);
1399 $trans->source($self->editor->requestor->ws_ou);
1400 $trans->dest($hold->pickup_lib);
1401 $trans->source_send_time("now");
1402 $trans->target_copy($copy->id);
1404 # when the copy gets to its destination, it will recover
1405 # this status - put it onto the holds shelf
1406 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1408 return $self->bail_on_events($self->editor->event)
1409 unless $self->editor->create_action_hold_transit_copy($trans);
1414 sub process_received_transit {
1416 my $copy = $self->copy;
1417 my $copyid = $self->copy->id;
1419 my $status_name = $U->copy_status($copy->status)->name;
1420 $logger->debug("circulator: attempting transit receive on ".
1421 "copy $copyid. Copy status is $status_name");
1423 my $transit = $self->transit;
1425 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1426 $logger->info("circulator: Fowarding transit on copy which is destined ".
1427 "for a different location. copy=$copyid,current ".
1428 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1430 return $self->bail_on_events(
1431 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1434 # The transit is received, set the receive time
1435 $transit->dest_recv_time('now');
1436 $self->bail_on_events($self->editor->event)
1437 unless $self->editor->update_action_transit_copy($transit);
1439 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1441 $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1442 $copy->status( $transit->copy_status );
1443 $self->update_copy();
1444 return if $self->bail_out;
1448 $self->do_hold_notify($hold_transit->hold);
1453 OpenILS::Event->new(
1456 payload => { transit => $transit, holdtransit => $hold_transit } ));
1458 return $hold_transit;
1462 sub checkin_handle_circ {
1466 my $circ = $self->circ;
1467 my $copy = $self->copy;
1471 # backdate the circ if necessary
1472 if($self->backdate) {
1473 $self->checkin_handle_backdate;
1474 return if $self->bail_out;
1477 if(!$circ->stop_fines) {
1478 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1479 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1480 $circ->stop_fines_time('now');
1483 # see if there are any fines owed on this circ. if not, close it
1484 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1485 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1487 # Set the checkin vars since we have the item
1488 $circ->checkin_time('now');
1489 $circ->checkin_staff($self->editor->requestor->id);
1490 $circ->checkin_lib($self->editor->requestor->ws_ou);
1492 my $circ_lib = (ref $self->copy->circ_lib) ?
1493 $self->copy->circ_lib->id : $self->copy->circ_lib;
1494 my $stat = $U->copy_status($self->copy->status)->id;
1496 # If the item is lost/missing and it needs to be sent home, don't
1497 # reshelve the copy, leave it lost/missing so the recipient will know
1498 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1499 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1500 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1503 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1508 return $self->bail_on_events($self->editor->event)
1509 unless $self->editor->update_action_circulation($circ);
1513 sub checkin_handle_backdate {
1516 my $bills = $self->editor->search_money_billing(
1518 billing_ts => { '>=' => $self->backdate },
1519 xact => $self->circ->id,
1520 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1524 for my $bill (@$bills) {
1525 if( !$bill->voided or $bill->voided =~ /f/i ) {
1527 my $n = $bill->note || "";
1528 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1530 $self->bail_on_events($self->editor->event)
1531 unless $self->editor->update_money_billing($bill);
1538 # XXX Legacy version for Circ.pm support
1539 sub _checkin_handle_backdate {
1540 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1542 my $bills = $session->request(
1543 "open-ils.storage.direct.money.billing.search_where.atomic",
1544 billing_ts => { '>=' => $backdate },
1546 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1550 for my $bill (@$bills) {
1552 my $n = $bill->note || "";
1553 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1554 my $s = $session->request(
1555 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1556 return $U->DB_UPDATE_FAILED($bill) unless $s;
1566 sub find_patron_from_copy {
1568 my $circs = $self->editor->search_action_circulation(
1569 { target_copy => $self->copy->id, checkin_time => undef });
1570 my $circ = $circs->[0];
1571 return unless $circ;
1572 my $u = $self->editor->retrieve_actor_user($circ->usr)
1573 or return $self->bail_on_events($self->editor->event);
1577 sub check_checkin_copy_status {
1579 my $copy = $self->copy;
1585 my $status = $U->copy_status($copy->status)->id;
1588 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1589 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1590 $status == OILS_COPY_STATUS_IN_PROCESS ||
1591 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1592 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1593 $status == OILS_COPY_STATUS_CATALOGING ||
1594 $status == OILS_COPY_STATUS_RESHELVING );
1596 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1597 if( $status == OILS_COPY_STATUS_LOST );
1599 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1600 if( $status == OILS_COPY_STATUS_MISSING );
1602 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1607 # --------------------------------------------------------------------------
1608 # On checkin, we need to return as many relevant objects as we can
1609 # --------------------------------------------------------------------------
1610 sub checkin_flesh_events {
1613 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1614 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1615 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1619 for my $evt (@{$self->events}) {
1622 $payload->{copy} = $U->unflesh_copy($self->copy);
1623 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1624 $payload->{circ} = $self->circ;
1625 $payload->{transit} = $self->transit;
1626 $payload->{hold} = $self->hold;
1628 $evt->{payload} = $payload;
1633 my( $self, $msg ) = @_;
1634 my $bc = ($self->copy) ? $self->copy->barcode :
1637 my $usr = ($self->patron) ? $self->patron->id : "";
1638 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1639 ", recipient=$usr, copy=$bc");
1645 $self->log_me("do_renew()");
1646 $self->is_renewal(1);
1648 unless( $self->is_renewal ) {
1649 return $self->bail_on_events($self->editor->events)
1650 unless $self->editor->allowed('RENEW_CIRC');
1653 # Make sure there is an open circ to renew that is not
1654 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1655 my $circ = $self->editor->search_action_circulation(
1656 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1658 return $self->bail_on_events($self->editor->event) unless $circ;
1660 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1661 if $circ->renewal_remaining < 1;
1663 # -----------------------------------------------------------------
1665 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1668 $self->run_renew_permit;
1671 $self->do_checkin();
1672 return if $self->bail_out;
1674 unless( $self->permit_override ) {
1676 return if $self->bail_out;
1677 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1678 $self->remove_event('ITEM_NOT_CATALOGED');
1681 $self->override_events;
1682 return if $self->bail_out;
1685 $self->do_checkout();
1690 my( $self, $evt ) = @_;
1691 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1692 $logger->debug("circulator: removing event from list: $evt");
1693 my @events = @{$self->events};
1694 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1699 my( $self, $evt ) = @_;
1700 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1701 return grep { $_->{textcode} eq $evt } @{$self->events};
1706 sub run_renew_permit {
1708 my $runner = $self->script_runner;
1710 $runner->load($self->circ_permit_renew);
1711 my $result = $runner->run or
1712 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1713 my $events = $result->{events};
1715 $logger->activity("circ_permit_renew for user ".
1716 $self->patron->id." returned events: @$events") if @$events;
1718 $self->push_events(OpenILS::Event->new($_)) for @$events;
1720 $logger->debug("circulator: re-creating script runner to be safe");
1721 $self->mk_script_runner;