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');
797 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
799 return $self->bail_on_events($self->editor->event)
800 unless $self->editor->update_asset_copy($self->copy);
802 $copy->status($U->copy_status($copy->status));
803 $copy->location($loc) if $loc;
804 $copy->circ_lib($circ_lib) if $circ_lib;
809 my( $self, @evts ) = @_;
810 $self->push_events(@evts);
814 sub handle_checkout_holds {
817 my $copy = $self->copy;
818 my $patron = $self->patron;
820 my $holds = $self->editor->search_action_hold_request(
822 current_copy => $copy->id ,
823 cancel_time => undef,
824 fulfillment_time => undef
830 # XXX We should only fulfill one hold here...
831 # XXX If a hold was transited to the user who is checking out
832 # the item, we need to make sure that hold is what's grabbed
835 # for now, just sort by id to get what should be the oldest hold
836 $holds = [ sort { $a->id <=> $b->id } @$holds ];
837 my @myholds = grep { $_->usr eq $patron->id } @$holds;
838 my @altholds = grep { $_->usr ne $patron->id } @$holds;
841 my $hold = $myholds[0];
843 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
845 # if the hold was never officially captured, capture it.
846 $hold->capture_time('now') unless $hold->capture_time;
848 # just make sure it's set correctly
849 $hold->current_copy($copy->id);
851 $hold->fulfillment_time('now');
852 $hold->fulfillment_staff($self->editor->requestor->id);
853 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
855 return $self->bail_on_events($self->editor->event)
856 unless $self->editor->update_action_hold_request($hold);
858 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
860 push( @fulfilled, $hold->id );
863 # If there are any holds placed for other users that point to this copy,
864 # then we need to un-target those holds so the targeter can pick a new copy
867 $logger->info("circulator: un-targeting hold ".$_->id.
868 " because copy ".$copy->id." is getting checked out");
870 # - make the targeter process this hold at next run
871 $_->clear_prev_check_time;
873 # - clear out the targetted copy
874 $_->clear_current_copy;
875 $_->clear_capture_time;
877 return $self->bail_on_event($self->editor->event)
878 unless $self->editor->update_action_hold_request($_);
882 $self->fulfilled_holds(\@fulfilled);
887 sub run_checkout_scripts {
891 my $runner = $self->script_runner;
892 $runner->load($self->circ_duration);
894 my $result = $runner->run or
895 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
897 my $duration = $result->{durationRule};
898 my $recurring = $result->{recurringFinesRule};
899 my $max_fine = $result->{maxFine};
901 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
903 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
904 return $self->bail_on_events($evt) if $evt;
906 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
907 return $self->bail_on_events($evt) if $evt;
909 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
910 return $self->bail_on_events($evt) if $evt;
914 # The item circulates with an unlimited duration
920 $self->duration_rule($duration);
921 $self->recurring_fines_rule($recurring);
922 $self->max_fine_rule($max_fine);
926 sub build_checkout_circ_object {
929 my $circ = Fieldmapper::action::circulation->new;
930 my $duration = $self->duration_rule;
931 my $max = $self->max_fine_rule;
932 my $recurring = $self->recurring_fines_rule;
933 my $copy = $self->copy;
934 my $patron = $self->patron;
938 my $dname = $duration->name;
939 my $mname = $max->name;
940 my $rname = $recurring->name;
942 $logger->debug("circulator: building circulation ".
943 "with duration=$dname, maxfine=$mname, recurring=$rname");
945 $circ->duration( $duration->shrt )
946 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
947 $circ->duration( $duration->normal )
948 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
949 $circ->duration( $duration->extended )
950 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
952 $circ->recuring_fine( $recurring->low )
953 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
954 $circ->recuring_fine( $recurring->normal )
955 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
956 $circ->recuring_fine( $recurring->high )
957 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
959 $circ->duration_rule( $duration->name );
960 $circ->recuring_fine_rule( $recurring->name );
961 $circ->max_fine_rule( $max->name );
962 $circ->max_fine( $max->amount );
964 $circ->fine_interval($recurring->recurance_interval);
965 $circ->renewal_remaining( $duration->max_renewals );
969 $logger->info("circulator: copy found with an unlimited circ duration");
970 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
971 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
972 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
973 $circ->renewal_remaining(0);
976 $circ->target_copy( $copy->id );
977 $circ->usr( $patron->id );
978 $circ->circ_lib( $self->circ_lib );
980 if( $self->is_renewal ) {
981 $circ->opac_renewal(1);
982 $circ->renewal_remaining($self->renewal_remaining);
983 $circ->circ_staff($self->editor->requestor->id);
986 # if the user provided an overiding checkout time,
987 # (e.g. the checkout really happened several hours ago), then
988 # we apply that here. Does this need a perm??
989 $circ->xact_start(clense_ISO8601($self->checkout_time))
990 if $self->checkout_time;
992 # if a patron is renewing, 'requestor' will be the patron
993 $circ->circ_staff($self->editor->requestor->id);
994 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1000 sub apply_modified_due_date {
1002 my $circ = $self->circ;
1003 my $copy = $self->copy;
1005 if( $self->due_date ) {
1007 return $self->bail_on_events($self->editor->event)
1008 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1010 $circ->due_date(clense_ISO8601($self->due_date));
1014 # if the due_date lands on a day when the location is closed
1015 return unless $copy and $circ->due_date;
1017 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1019 $logger->info("circ searching for closed date overlap on lib $org".
1020 " with an item due date of ".$circ->due_date );
1022 my $dateinfo = $U->storagereq(
1023 'open-ils.storage.actor.org_unit.closed_date.overlap',
1024 $org, $circ->due_date );
1027 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1028 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1030 # XXX make the behavior more dynamic
1031 # for now, we just push the due date to after the close date
1032 $circ->due_date($dateinfo->{end});
1039 sub create_due_date {
1040 my( $self, $duration ) = @_;
1041 my ($sec,$min,$hour,$mday,$mon,$year) =
1042 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1043 $year += 1900; $mon += 1;
1044 my $due_date = sprintf(
1045 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1046 $year, $mon, $mday, $hour, $min, $sec);
1052 sub make_precat_copy {
1054 my $copy = $self->copy;
1057 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1059 $copy->editor($self->editor->requestor->id);
1060 $copy->edit_date('now');
1061 $copy->dummy_title($self->dummy_title);
1062 $copy->dummy_author($self->dummy_author);
1064 $self->update_copy();
1068 $logger->info("circulator: Creating a new precataloged ".
1069 "copy in checkout with barcode " . $self->copy_barcode);
1071 $copy = Fieldmapper::asset::copy->new;
1072 $copy->circ_lib($self->circ_lib);
1073 $copy->creator($self->editor->requestor->id);
1074 $copy->editor($self->editor->requestor->id);
1075 $copy->barcode($self->copy_barcode);
1076 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1077 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1078 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1080 $copy->dummy_title($self->dummy_title || "");
1081 $copy->dummy_author($self->dummy_author || "");
1083 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1085 $self->push_events($self->editor->event);
1089 # this is a little bit of a hack, but we need to
1090 # get the copy into the script runner
1091 $self->script_runner->insert("environment.copy", $copy, 1);
1095 sub checkout_noncat {
1101 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1102 my $count = $self->noncat_count || 1;
1103 my $cotime = clense_ISO8601($self->checkout_time) || "";
1105 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1109 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1110 $self->editor->requestor->id,
1118 $self->push_events($evt);
1129 $self->log_me("do_checkin()");
1131 return $self->bail_on_events(
1132 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1135 unless( $self->is_renewal ) {
1136 return $self->bail_on_events($self->editor->event)
1137 unless $self->editor->allowed('COPY_CHECKIN');
1140 $self->push_events($self->check_copy_alert());
1141 $self->push_events($self->check_checkin_copy_status());
1143 # the renew code will have already found our circulation object
1144 unless( $self->is_renewal and $self->circ ) {
1146 $self->editor->search_action_circulation(
1147 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1150 # if the circ is marked as 'claims returned', add the event to the list
1151 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1152 if ($self->circ and $self->circ->stop_fines
1153 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1155 # handle the overridable events
1156 $self->override_events unless $self->is_renewal;
1157 return if $self->bail_out;
1161 $self->editor->search_action_transit_copy(
1162 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1166 $self->checkin_handle_circ;
1167 return if $self->bail_out;
1168 $self->checkin_changed(1);
1170 } elsif( $self->transit ) {
1171 my $hold_transit = $self->process_received_transit;
1172 $self->checkin_changed(1);
1174 if( $self->bail_out ) {
1175 $self->checkin_flesh_events;
1179 if( my $e = $self->check_checkin_copy_status() ) {
1180 # If the original copy status is special, alert the caller
1181 my $ev = $self->events;
1182 $self->events([$e]);
1183 $self->override_events;
1184 return if $self->bail_out;
1189 if( $hold_transit or
1190 $U->copy_status($self->copy->status)->id
1191 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1192 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1193 $self->checkin_flesh_events;
1198 if( $self->is_renewal ) {
1199 $self->push_events(OpenILS::Event->new('SUCCESS'));
1203 # ------------------------------------------------------------------------------
1204 # Circulations and transits are now closed where necessary. Now go on to see if
1205 # this copy can fulfill a hold or needs to be routed to a different location
1206 # ------------------------------------------------------------------------------
1208 if( $self->attempt_checkin_hold_capture() ) {
1209 return if $self->bail_out;
1211 } else { # not needed for a hold
1213 my $circ_lib = (ref $self->copy->circ_lib) ?
1214 $self->copy->circ_lib->id : $self->copy->circ_lib;
1216 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1218 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1220 $self->checkin_handle_precat();
1221 return if $self->bail_out;
1225 $self->checkin_build_copy_transit();
1226 return if $self->bail_out;
1227 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1232 $self->reshelve_copy;
1233 return if $self->bail_out;
1235 unless($self->checkin_changed) {
1237 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1238 my $stat = $U->copy_status($self->copy->status)->id;
1240 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1241 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1242 $self->bail_out(1); # no need to commit anything
1245 $self->push_events(OpenILS::Event->new('SUCCESS'))
1246 unless @{$self->events};
1249 $self->checkin_flesh_events;
1255 my $force = $self->force || shift;
1256 my $copy = $self->copy;
1258 my $stat = $U->copy_status($copy->status)->id;
1261 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1262 $stat != OILS_COPY_STATUS_CATALOGING and
1263 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1264 $stat != OILS_COPY_STATUS_RESHELVING )) {
1266 $copy->status( OILS_COPY_STATUS_RESHELVING );
1268 $self->checkin_changed(1);
1273 sub checkin_handle_precat {
1275 my $copy = $self->copy;
1277 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1278 $copy->status(OILS_COPY_STATUS_CATALOGING);
1279 $self->update_copy();
1280 $self->checkin_changed(1);
1281 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1286 sub checkin_build_copy_transit {
1288 my $copy = $self->copy;
1289 my $transit = Fieldmapper::action::transit_copy->new;
1291 $transit->source($self->editor->requestor->ws_ou);
1292 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1293 $transit->target_copy($copy->id);
1294 $transit->source_send_time('now');
1295 $transit->copy_status( $U->copy_status($copy->status)->id );
1297 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1299 return $self->bail_on_events($self->editor->event)
1300 unless $self->editor->create_action_transit_copy($transit);
1302 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1304 $self->checkin_changed(1);
1308 sub attempt_checkin_hold_capture {
1310 my $copy = $self->copy;
1312 # See if this copy can fulfill any holds
1313 my ($hold) = $holdcode->find_nearest_permitted_hold(
1314 OpenSRF::AppSession->create('open-ils.storage'),
1315 $copy, $self->editor->requestor );
1318 $logger->debug("circulator: no potential permitted".
1319 "holds found for copy ".$copy->barcode);
1324 $logger->info("circulator: found permitted hold ".
1325 $hold->id . " for copy, capturing...");
1327 $hold->current_copy($copy->id);
1328 $hold->capture_time('now');
1330 # prevent DB errors caused by fetching
1331 # holds from storage, and updating through cstore
1332 $hold->clear_fulfillment_time;
1333 $hold->clear_fulfillment_staff;
1334 $hold->clear_fulfillment_lib;
1335 $hold->clear_expire_time;
1336 $hold->clear_cancel_time;
1337 $hold->clear_prev_check_time unless $hold->prev_check_time;
1339 $self->bail_on_events($self->editor->event)
1340 unless $self->editor->update_action_hold_request($hold);
1342 $self->checkin_changed(1);
1344 return 1 if $self->bail_out;
1346 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1348 # This hold was captured in the correct location
1349 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1350 $self->push_events(OpenILS::Event->new('SUCCESS'));
1352 $self->do_hold_notify($hold->id);
1356 # Hold needs to be picked up elsewhere. Build a hold
1357 # transit and route the item.
1358 $self->checkin_build_hold_transit();
1359 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1360 return 1 if $self->bail_out;
1362 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1365 # make sure we save the copy status
1370 sub do_hold_notify {
1371 my( $self, $holdid ) = @_;
1372 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1373 editor => $self->editor, hold_id => $holdid );
1375 if(!$notifier->event) {
1377 $logger->info("attempt at sending hold notification for hold $holdid");
1379 my $stat = $notifier->send_email_notify;
1380 $logger->info("hold notify succeeded for hold $holdid") if $stat eq '1';
1381 $logger->warn(" * hold notify failed for hold $holdid") if $stat ne '1';
1384 $logger->info("Not sending hold notification since the patron has no email address");
1389 sub checkin_build_hold_transit {
1393 my $copy = $self->copy;
1394 my $hold = $self->hold;
1395 my $trans = Fieldmapper::action::hold_transit_copy->new;
1397 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1399 $trans->hold($hold->id);
1400 $trans->source($self->editor->requestor->ws_ou);
1401 $trans->dest($hold->pickup_lib);
1402 $trans->source_send_time("now");
1403 $trans->target_copy($copy->id);
1405 # when the copy gets to its destination, it will recover
1406 # this status - put it onto the holds shelf
1407 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1409 return $self->bail_on_events($self->editor->event)
1410 unless $self->editor->create_action_hold_transit_copy($trans);
1415 sub process_received_transit {
1417 my $copy = $self->copy;
1418 my $copyid = $self->copy->id;
1420 my $status_name = $U->copy_status($copy->status)->name;
1421 $logger->debug("circulator: attempting transit receive on ".
1422 "copy $copyid. Copy status is $status_name");
1424 my $transit = $self->transit;
1426 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1427 $logger->info("circulator: Fowarding transit on copy which is destined ".
1428 "for a different location. copy=$copyid,current ".
1429 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1431 return $self->bail_on_events(
1432 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1435 # The transit is received, set the receive time
1436 $transit->dest_recv_time('now');
1437 $self->bail_on_events($self->editor->event)
1438 unless $self->editor->update_action_transit_copy($transit);
1440 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1442 $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1443 $copy->status( $transit->copy_status );
1444 $self->update_copy();
1445 return if $self->bail_out;
1449 $self->do_hold_notify($hold_transit->hold);
1454 OpenILS::Event->new(
1457 payload => { transit => $transit, holdtransit => $hold_transit } ));
1459 return $hold_transit;
1463 sub checkin_handle_circ {
1467 my $circ = $self->circ;
1468 my $copy = $self->copy;
1472 # backdate the circ if necessary
1473 if($self->backdate) {
1474 $self->checkin_handle_backdate;
1475 return if $self->bail_out;
1478 if(!$circ->stop_fines) {
1479 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1480 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1481 $circ->stop_fines_time('now');
1484 # see if there are any fines owed on this circ. if not, close it
1485 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1486 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1488 # Set the checkin vars since we have the item
1489 $circ->checkin_time('now');
1490 $circ->checkin_staff($self->editor->requestor->id);
1491 $circ->checkin_lib($self->editor->requestor->ws_ou);
1493 my $circ_lib = (ref $self->copy->circ_lib) ?
1494 $self->copy->circ_lib->id : $self->copy->circ_lib;
1495 my $stat = $U->copy_status($self->copy->status)->id;
1497 # If the item is lost/missing and it needs to be sent home, don't
1498 # reshelve the copy, leave it lost/missing so the recipient will know
1499 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1500 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1501 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1504 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1509 return $self->bail_on_events($self->editor->event)
1510 unless $self->editor->update_action_circulation($circ);
1514 sub checkin_handle_backdate {
1517 my $bills = $self->editor->search_money_billing(
1519 billing_ts => { '>=' => $self->backdate },
1520 xact => $self->circ->id,
1521 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1525 for my $bill (@$bills) {
1526 if( !$bill->voided or $bill->voided =~ /f/i ) {
1528 my $n = $bill->note || "";
1529 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1531 $self->bail_on_events($self->editor->event)
1532 unless $self->editor->update_money_billing($bill);
1539 # XXX Legacy version for Circ.pm support
1540 sub _checkin_handle_backdate {
1541 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1543 my $bills = $session->request(
1544 "open-ils.storage.direct.money.billing.search_where.atomic",
1545 billing_ts => { '>=' => $backdate },
1547 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1551 for my $bill (@$bills) {
1553 my $n = $bill->note || "";
1554 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1555 my $s = $session->request(
1556 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1557 return $U->DB_UPDATE_FAILED($bill) unless $s;
1567 sub find_patron_from_copy {
1569 my $circs = $self->editor->search_action_circulation(
1570 { target_copy => $self->copy->id, checkin_time => undef });
1571 my $circ = $circs->[0];
1572 return unless $circ;
1573 my $u = $self->editor->retrieve_actor_user($circ->usr)
1574 or return $self->bail_on_events($self->editor->event);
1578 sub check_checkin_copy_status {
1580 my $copy = $self->copy;
1586 my $status = $U->copy_status($copy->status)->id;
1589 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1590 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1591 $status == OILS_COPY_STATUS_IN_PROCESS ||
1592 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1593 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1594 $status == OILS_COPY_STATUS_CATALOGING ||
1595 $status == OILS_COPY_STATUS_RESHELVING );
1597 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1598 if( $status == OILS_COPY_STATUS_LOST );
1600 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1601 if( $status == OILS_COPY_STATUS_MISSING );
1603 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1608 # --------------------------------------------------------------------------
1609 # On checkin, we need to return as many relevant objects as we can
1610 # --------------------------------------------------------------------------
1611 sub checkin_flesh_events {
1614 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1615 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1616 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1620 for my $evt (@{$self->events}) {
1623 $payload->{copy} = $U->unflesh_copy($self->copy);
1624 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1625 $payload->{circ} = $self->circ;
1626 $payload->{transit} = $self->transit;
1627 $payload->{hold} = $self->hold;
1629 $evt->{payload} = $payload;
1634 my( $self, $msg ) = @_;
1635 my $bc = ($self->copy) ? $self->copy->barcode :
1638 my $usr = ($self->patron) ? $self->patron->id : "";
1639 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1640 ", recipient=$usr, copy=$bc");
1646 $self->log_me("do_renew()");
1647 $self->is_renewal(1);
1649 unless( $self->is_renewal ) {
1650 return $self->bail_on_events($self->editor->events)
1651 unless $self->editor->allowed('RENEW_CIRC');
1654 # Make sure there is an open circ to renew that is not
1655 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1656 my $circ = $self->editor->search_action_circulation(
1657 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1659 return $self->bail_on_events($self->editor->event) unless $circ;
1661 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1662 if $circ->renewal_remaining < 1;
1664 # -----------------------------------------------------------------
1666 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1669 $self->run_renew_permit;
1672 $self->do_checkin();
1673 return if $self->bail_out;
1675 unless( $self->permit_override ) {
1677 return if $self->bail_out;
1678 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1679 $self->remove_event('ITEM_NOT_CATALOGED');
1682 $self->override_events;
1683 return if $self->bail_out;
1686 $self->do_checkout();
1691 my( $self, $evt ) = @_;
1692 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1693 $logger->debug("circulator: removing event from list: $evt");
1694 my @events = @{$self->events};
1695 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1700 my( $self, $evt ) = @_;
1701 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1702 return grep { $_->{textcode} eq $evt } @{$self->events};
1707 sub run_renew_permit {
1709 my $runner = $self->script_runner;
1711 $runner->load($self->circ_permit_renew);
1712 my $result = $runner->run or
1713 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1714 my $events = $result->{events};
1716 $logger->activity("circ_permit_renew for user ".
1717 $self->patron->id." returned events: @$events") if @$events;
1719 $self->push_events(OpenILS::Event->new($_)) for @$events;
1721 $logger->debug("circulator: re-creating script runner to be safe");
1722 $self->mk_script_runner;