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 $self->script_runner->add_path( $_ ) for @$script_libs;
476 # --------------------------------------------------------------------------
477 # Does the circ permit work
478 # --------------------------------------------------------------------------
482 $self->log_me("do_permit()");
484 unless( $self->editor->requestor->id == $self->patron->id ) {
485 return $self->bail_on_events($self->editor->event)
486 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
489 $self->do_copy_checks();
490 return if $self->bail_out;
491 $self->run_patron_permit_scripts();
492 $self->run_copy_permit_scripts()
493 unless $self->is_precat or $self->is_noncat;
494 $self->override_events() unless $self->is_renewal;
495 return if $self->bail_out;
497 if( $self->is_precat ) {
500 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
501 return $self->bail_out(1) unless $self->is_renewal;
507 payload => $self->mk_permit_key));
513 my $copy = $self->copy;
516 my $stat = $U->copy_status($copy->status)->id;
518 # We cannot check out a copy if it is in-transit
519 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
520 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
523 $self->handle_claims_returned();
524 return if $self->bail_out;
526 # no claims returned circ was found, check if there is any open circ
527 unless( $self->is_renewal ) {
528 my $circs = $self->editor->search_action_circulation(
529 { target_copy => $copy->id, checkin_time => undef }
532 return $self->bail_on_events(
533 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
538 # ---------------------------------------------------------------------
539 # This pushes any patron-related events into the list but does not
540 # set bail_out for any events
541 # ---------------------------------------------------------------------
542 sub run_patron_permit_scripts {
544 my $runner = $self->script_runner;
545 my $patronid = $self->patron->id;
547 # ---------------------------------------------------------------------
548 # Find all of the fatal penalties currently set on the user
549 # ---------------------------------------------------------------------
550 my $penalties = $U->update_patron_penalties(
551 authtoken => $self->editor->authtoken,
552 patron => $self->patron,
555 $penalties = $penalties->{fatal_penalties};
558 # ---------------------------------------------------------------------
559 # Now run the patron permit script
560 # ---------------------------------------------------------------------
561 $runner->load($self->circ_permit_patron);
562 my $result = $runner->run or
563 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
565 my $patron_events = $result->{events};
567 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
569 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
571 $self->push_events(@allevents);
575 sub run_copy_permit_scripts {
577 my $copy = $self->copy || return;
578 my $runner = $self->script_runner;
580 # ---------------------------------------------------------------------
581 # Capture all of the copy permit events
582 # ---------------------------------------------------------------------
583 $runner->load($self->circ_permit_copy);
584 my $result = $runner->run or
585 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
586 my $copy_events = $result->{events};
588 # ---------------------------------------------------------------------
589 # Now collect all of the events together
590 # ---------------------------------------------------------------------
592 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
594 # See if this copy has an alert message
595 my $ae = $self->check_copy_alert();
596 push( @allevents, $ae ) if $ae;
598 # uniquify the events
599 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
600 @allevents = values %hash;
603 $_->{payload} = $copy if
604 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
607 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
609 $self->push_events(@allevents);
613 sub check_copy_alert {
615 return OpenILS::Event->new(
616 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
617 if $self->copy and $self->copy->alert_message;
623 # --------------------------------------------------------------------------
624 # If the call is overriding and has permissions to override every collected
625 # event, the are cleared. Any event that the caller does not have
626 # permission to override, will be left in the event list and bail_out will
628 # XXX We need code in here to cancel any holds/transits on copies
629 # that are being force-checked out
630 # --------------------------------------------------------------------------
631 sub override_events {
633 my @events = @{$self->events};
634 return unless @events;
636 if(!$self->override) {
637 return $self->bail_out(1)
638 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
643 for my $e (@events) {
644 my $tc = $e->{textcode};
645 next if $tc eq 'SUCCESS';
646 my $ov = "$tc.override";
647 $logger->info("circulator: attempting to override event: $ov");
649 return $self->bail_on_events($self->editor->event)
650 unless( $self->editor->allowed($ov) );
655 # --------------------------------------------------------------------------
656 # If there is an open claimsreturn circ on the requested copy, close the
657 # circ if overriding, otherwise bail out
658 # --------------------------------------------------------------------------
659 sub handle_claims_returned {
661 my $copy = $self->copy;
663 my $CR = $self->editor->search_action_circulation(
665 target_copy => $copy->id,
666 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
667 checkin_time => undef,
671 return unless ($CR = $CR->[0]);
675 # - If the caller has set the override flag, we will check the item in
676 if($self->override) {
678 $CR->checkin_time('now');
679 $CR->checkin_lib($self->editor->requestor->ws_ou);
680 $CR->checkin_staff($self->editor->requestor->id);
682 $evt = $self->editor->event
683 unless $self->editor->update_action_circulation($CR);
686 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
689 $self->bail_on_events($evt) if $evt;
694 # --------------------------------------------------------------------------
695 # This performs the checkout
696 # --------------------------------------------------------------------------
700 $self->log_me("do_checkout()");
702 # make sure perms are good if this isn't a renewal
703 unless( $self->is_renewal ) {
704 return $self->bail_on_events($self->editor->event)
705 unless( $self->editor->allowed('COPY_CHECKOUT') );
708 # verify the permit key
709 unless( $self->check_permit_key ) {
710 if( $self->permit_override ) {
711 return $self->bail_on_events($self->editor->event)
712 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
714 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
718 # if this is a non-cataloged circ, build the circ and finish
719 if( $self->is_noncat ) {
720 $self->checkout_noncat;
722 OpenILS::Event->new('SUCCESS',
723 payload => { noncat_circ => $self->circ }));
727 if( $self->is_precat ) {
728 $self->script_runner->insert("environment.isPrecat", 1, 1);
729 $self->make_precat_copy;
730 return if $self->bail_out;
732 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
733 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
736 $self->do_copy_checks;
737 return if $self->bail_out;
739 $self->run_checkout_scripts();
740 return if $self->bail_out;
742 $self->build_checkout_circ_object();
743 return if $self->bail_out;
745 $self->apply_modified_due_date();
746 return if $self->bail_out;
748 return $self->bail_on_events($self->editor->event)
749 unless $self->editor->create_action_circulation($self->circ);
751 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
753 return if $self->bail_out;
755 $self->handle_checkout_holds();
756 return if $self->bail_out;
758 # ------------------------------------------------------------------------------
759 # Update the patron penalty info in the DB
760 # ------------------------------------------------------------------------------
761 $U->update_patron_penalties(
762 authtoken => $self->editor->authtoken,
763 patron => $self->patron,
767 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
769 OpenILS::Event->new('SUCCESS',
771 copy => $U->unflesh_copy($self->copy),
774 holds_fulfilled => $self->fulfilled_holds,
782 my $copy = $self->copy;
784 my $stat = $copy->status if ref $copy->status;
785 my $loc = $copy->location if ref $copy->location;
786 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
788 $copy->status($stat->id) if $stat;
789 $copy->location($loc->id) if $loc;
790 $copy->circ_lib($circ_lib->id) if $circ_lib;
791 $copy->editor($self->editor->requestor->id);
792 $copy->edit_date('now');
794 return $self->bail_on_events($self->editor->event)
795 unless $self->editor->update_asset_copy($self->copy);
797 $copy->status($U->copy_status($copy->status));
798 $copy->location($loc) if $loc;
799 $copy->circ_lib($circ_lib) if $circ_lib;
804 my( $self, @evts ) = @_;
805 $self->push_events(@evts);
809 sub handle_checkout_holds {
812 my $copy = $self->copy;
813 my $patron = $self->patron;
815 my $holds = $self->editor->search_action_hold_request(
817 current_copy => $copy->id ,
818 cancel_time => undef,
819 fulfillment_time => undef
825 # XXX We should only fulfill one hold here...
826 # XXX If a hold was transited to the user who is checking out
827 # the item, we need to make sure that hold is what's grabbed
830 # for now, just sort by id to get what should be the oldest hold
831 $holds = [ sort { $a->id <=> $b->id } @$holds ];
832 my @myholds = grep { $_->usr eq $patron->id } @$holds;
833 my @altholds = grep { $_->usr ne $patron->id } @$holds;
836 my $hold = $myholds[0];
838 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
840 # if the hold was never officially captured, capture it.
841 $hold->capture_time('now') unless $hold->capture_time;
843 # just make sure it's set correctly
844 $hold->current_copy($copy->id);
846 $hold->fulfillment_time('now');
847 $hold->fulfillment_staff($self->editor->requestor->id);
848 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
850 return $self->bail_on_events($self->editor->event)
851 unless $self->editor->update_action_hold_request($hold);
853 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
855 push( @fulfilled, $hold->id );
858 # If there are any holds placed for other users that point to this copy,
859 # then we need to un-target those holds so the targeter can pick a new copy
862 $logger->info("circulator: un-targeting hold ".$_->id.
863 " because copy ".$copy->id." is getting checked out");
865 # - make the targeter process this hold at next run
866 $_->clear_prev_check_time;
868 # - clear out the targetted copy
869 $_->clear_current_copy;
870 $_->clear_capture_time;
872 return $self->bail_on_event($self->editor->event)
873 unless $self->editor->update_action_hold_request($_);
877 $self->fulfilled_holds(\@fulfilled);
882 sub run_checkout_scripts {
886 my $runner = $self->script_runner;
887 $runner->load($self->circ_duration);
889 my $result = $runner->run or
890 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
892 my $duration = $result->{durationRule};
893 my $recurring = $result->{recurringFinesRule};
894 my $max_fine = $result->{maxFine};
896 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
898 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
899 return $self->bail_on_events($evt) if $evt;
901 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
902 return $self->bail_on_events($evt) if $evt;
904 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
905 return $self->bail_on_events($evt) if $evt;
909 # The item circulates with an unlimited duration
915 $self->duration_rule($duration);
916 $self->recurring_fines_rule($recurring);
917 $self->max_fine_rule($max_fine);
921 sub build_checkout_circ_object {
924 my $circ = Fieldmapper::action::circulation->new;
925 my $duration = $self->duration_rule;
926 my $max = $self->max_fine_rule;
927 my $recurring = $self->recurring_fines_rule;
928 my $copy = $self->copy;
929 my $patron = $self->patron;
933 my $dname = $duration->name;
934 my $mname = $max->name;
935 my $rname = $recurring->name;
937 $logger->debug("circulator: building circulation ".
938 "with duration=$dname, maxfine=$mname, recurring=$rname");
940 $circ->duration( $duration->shrt )
941 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
942 $circ->duration( $duration->normal )
943 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
944 $circ->duration( $duration->extended )
945 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
947 $circ->recuring_fine( $recurring->low )
948 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
949 $circ->recuring_fine( $recurring->normal )
950 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
951 $circ->recuring_fine( $recurring->high )
952 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
954 $circ->duration_rule( $duration->name );
955 $circ->recuring_fine_rule( $recurring->name );
956 $circ->max_fine_rule( $max->name );
957 $circ->max_fine( $max->amount );
959 $circ->fine_interval($recurring->recurance_interval);
960 $circ->renewal_remaining( $duration->max_renewals );
964 $logger->info("circulator: copy found with an unlimited circ duration");
965 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
966 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
967 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
968 $circ->renewal_remaining(0);
971 $circ->target_copy( $copy->id );
972 $circ->usr( $patron->id );
973 $circ->circ_lib( $self->circ_lib );
975 if( $self->is_renewal ) {
976 $circ->opac_renewal(1);
977 $circ->renewal_remaining($self->renewal_remaining);
978 $circ->circ_staff($self->editor->requestor->id);
981 # if the user provided an overiding checkout time,
982 # (e.g. the checkout really happened several hours ago), then
983 # we apply that here. Does this need a perm??
984 $circ->xact_start(clense_ISO8601($self->checkout_time))
985 if $self->checkout_time;
987 # if a patron is renewing, 'requestor' will be the patron
988 $circ->circ_staff($self->editor->requestor->id);
989 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
995 sub apply_modified_due_date {
997 my $circ = $self->circ;
998 my $copy = $self->copy;
1000 if( $self->due_date ) {
1002 return $self->bail_on_events($self->editor->event)
1003 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1005 $circ->due_date(clense_ISO8601($self->due_date));
1009 # if the due_date lands on a day when the location is closed
1010 return unless $copy and $circ->due_date;
1012 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1014 $logger->info("circ searching for closed date overlap on lib $org".
1015 " with an item due date of ".$circ->due_date );
1017 my $dateinfo = $U->storagereq(
1018 'open-ils.storage.actor.org_unit.closed_date.overlap',
1019 $org, $circ->due_date );
1022 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1023 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1025 # XXX make the behavior more dynamic
1026 # for now, we just push the due date to after the close date
1027 $circ->due_date($dateinfo->{end});
1034 sub create_due_date {
1035 my( $self, $duration ) = @_;
1036 my ($sec,$min,$hour,$mday,$mon,$year) =
1037 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1038 $year += 1900; $mon += 1;
1039 my $due_date = sprintf(
1040 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1041 $year, $mon, $mday, $hour, $min, $sec);
1047 sub make_precat_copy {
1049 my $copy = $self->copy;
1052 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1054 $copy->editor($self->editor->requestor->id);
1055 $copy->edit_date('now');
1056 $copy->dummy_title($self->dummy_title);
1057 $copy->dummy_author($self->dummy_author);
1059 $self->update_copy();
1063 $logger->info("circulator: Creating a new precataloged ".
1064 "copy in checkout with barcode " . $self->copy_barcode);
1066 $copy = Fieldmapper::asset::copy->new;
1067 $copy->circ_lib($self->circ_lib);
1068 $copy->creator($self->editor->requestor->id);
1069 $copy->editor($self->editor->requestor->id);
1070 $copy->barcode($self->copy_barcode);
1071 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1072 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1073 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1075 $copy->dummy_title($self->dummy_title || "");
1076 $copy->dummy_author($self->dummy_author || "");
1078 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1080 $self->push_events($self->editor->event);
1084 # this is a little bit of a hack, but we need to
1085 # get the copy into the script runner
1086 $self->script_runner->insert("environment.copy", $copy, 1);
1090 sub checkout_noncat {
1096 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1097 my $count = $self->noncat_count || 1;
1098 my $cotime = clense_ISO8601($self->checkout_time) || "";
1100 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1104 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1105 $self->editor->requestor->id,
1113 $self->push_events($evt);
1124 $self->log_me("do_checkin()");
1126 return $self->bail_on_events(
1127 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1130 unless( $self->is_renewal ) {
1131 return $self->bail_on_events($self->editor->event)
1132 unless $self->editor->allowed('COPY_CHECKIN');
1135 $self->push_events($self->check_copy_alert());
1136 $self->push_events($self->check_checkin_copy_status());
1138 # the renew code will have already found our circulation object
1139 unless( $self->is_renewal and $self->circ ) {
1141 $self->editor->search_action_circulation(
1142 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1145 # if the circ is marked as 'claims returned', add the event to the list
1146 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1147 if ($self->circ and $self->circ->stop_fines
1148 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1150 # handle the overridable events
1151 $self->override_events unless $self->is_renewal;
1152 return if $self->bail_out;
1156 $self->editor->search_action_transit_copy(
1157 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1161 $self->checkin_handle_circ;
1162 return if $self->bail_out;
1163 $self->checkin_changed(1);
1165 } elsif( $self->transit ) {
1166 my $hold_transit = $self->process_received_transit;
1167 $self->checkin_changed(1);
1169 if( $self->bail_out ) {
1170 $self->checkin_flesh_events;
1174 if( my $e = $self->check_checkin_copy_status() ) {
1175 # If the original copy status is special, alert the caller
1176 my $ev = $self->events;
1177 $self->events([$e]);
1178 $self->override_events;
1179 return if $self->bail_out;
1184 if( $hold_transit or
1185 $U->copy_status($self->copy->status)->id
1186 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1187 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1188 $self->checkin_flesh_events;
1193 if( $self->is_renewal ) {
1194 $self->push_events(OpenILS::Event->new('SUCCESS'));
1198 # ------------------------------------------------------------------------------
1199 # Circulations and transits are now closed where necessary. Now go on to see if
1200 # this copy can fulfill a hold or needs to be routed to a different location
1201 # ------------------------------------------------------------------------------
1203 if( $self->attempt_checkin_hold_capture() ) {
1204 return if $self->bail_out;
1206 } else { # not needed for a hold
1208 my $circ_lib = (ref $self->copy->circ_lib) ?
1209 $self->copy->circ_lib->id : $self->copy->circ_lib;
1211 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1213 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1215 $self->checkin_handle_precat();
1216 return if $self->bail_out;
1220 $self->checkin_build_copy_transit();
1221 return if $self->bail_out;
1222 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1227 $self->reshelve_copy;
1228 return if $self->bail_out;
1230 unless($self->checkin_changed) {
1232 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1233 my $stat = $U->copy_status($self->copy->status)->id;
1235 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1236 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1237 $self->bail_out(1); # no need to commit anything
1240 $self->push_events(OpenILS::Event->new('SUCCESS'))
1241 unless @{$self->events};
1244 $self->checkin_flesh_events;
1250 my $force = $self->force || shift;
1251 my $copy = $self->copy;
1253 my $stat = $U->copy_status($copy->status)->id;
1256 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1257 $stat != OILS_COPY_STATUS_CATALOGING and
1258 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1259 $stat != OILS_COPY_STATUS_RESHELVING )) {
1261 $copy->status( OILS_COPY_STATUS_RESHELVING );
1263 $self->checkin_changed(1);
1268 sub checkin_handle_precat {
1270 my $copy = $self->copy;
1272 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1273 $copy->status(OILS_COPY_STATUS_CATALOGING);
1274 $self->update_copy();
1275 $self->checkin_changed(1);
1276 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1281 sub checkin_build_copy_transit {
1283 my $copy = $self->copy;
1284 my $transit = Fieldmapper::action::transit_copy->new;
1286 $transit->source($self->editor->requestor->ws_ou);
1287 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1288 $transit->target_copy($copy->id);
1289 $transit->source_send_time('now');
1290 $transit->copy_status( $U->copy_status($copy->status)->id );
1292 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1294 return $self->bail_on_events($self->editor->event)
1295 unless $self->editor->create_action_transit_copy($transit);
1297 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1299 $self->checkin_changed(1);
1303 sub attempt_checkin_hold_capture {
1305 my $copy = $self->copy;
1307 # See if this copy can fulfill any holds
1308 my ($hold) = $holdcode->find_nearest_permitted_hold(
1309 OpenSRF::AppSession->create('open-ils.storage'),
1310 $copy, $self->editor->requestor );
1313 $logger->debug("circulator: no potential permitted".
1314 "holds found for copy ".$copy->barcode);
1319 $logger->info("circulator: found permitted hold ".
1320 $hold->id . " for copy, capturing...");
1322 $hold->current_copy($copy->id);
1323 $hold->capture_time('now');
1325 # prevent DB errors caused by fetching
1326 # holds from storage, and updating through cstore
1327 $hold->clear_fulfillment_time;
1328 $hold->clear_fulfillment_staff;
1329 $hold->clear_fulfillment_lib;
1330 $hold->clear_expire_time;
1331 $hold->clear_cancel_time;
1332 $hold->clear_prev_check_time unless $hold->prev_check_time;
1334 $self->bail_on_events($self->editor->event)
1335 unless $self->editor->update_action_hold_request($hold);
1337 $self->checkin_changed(1);
1339 return 1 if $self->bail_out;
1341 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1343 # This hold was captured in the correct location
1344 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1345 $self->push_events(OpenILS::Event->new('SUCCESS'));
1347 $self->do_hold_notify($hold->id);
1351 # Hold needs to be picked up elsewhere. Build a hold
1352 # transit and route the item.
1353 $self->checkin_build_hold_transit();
1354 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1355 return 1 if $self->bail_out;
1357 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1360 # make sure we save the copy status
1365 sub do_hold_notify {
1366 my( $self, $holdid ) = @_;
1367 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1368 editor => $self->editor, hold_id => $holdid );
1370 if(!$notifier->event) {
1372 $logger->info("attempt at sending hold notification for hold $holdid");
1374 my $stat = $notifier->send_email_notify;
1375 $logger->info("hold notify succeeded for hold $holdid") if $stat eq '1';
1376 $logger->warn(" * hold notify failed for hold $holdid") if $stat ne '1';
1379 $logger->info("Not sending hold notification since the patron has no email address");
1384 sub checkin_build_hold_transit {
1388 my $copy = $self->copy;
1389 my $hold = $self->hold;
1390 my $trans = Fieldmapper::action::hold_transit_copy->new;
1392 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1394 $trans->hold($hold->id);
1395 $trans->source($self->editor->requestor->ws_ou);
1396 $trans->dest($hold->pickup_lib);
1397 $trans->source_send_time("now");
1398 $trans->target_copy($copy->id);
1400 # when the copy gets to its destination, it will recover
1401 # this status - put it onto the holds shelf
1402 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1404 return $self->bail_on_events($self->editor->event)
1405 unless $self->editor->create_action_hold_transit_copy($trans);
1410 sub process_received_transit {
1412 my $copy = $self->copy;
1413 my $copyid = $self->copy->id;
1415 my $status_name = $U->copy_status($copy->status)->name;
1416 $logger->debug("circulator: attempting transit receive on ".
1417 "copy $copyid. Copy status is $status_name");
1419 my $transit = $self->transit;
1421 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1422 $logger->info("circulator: Fowarding transit on copy which is destined ".
1423 "for a different location. copy=$copyid,current ".
1424 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1426 return $self->bail_on_events(
1427 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1430 # The transit is received, set the receive time
1431 $transit->dest_recv_time('now');
1432 $self->bail_on_events($self->editor->event)
1433 unless $self->editor->update_action_transit_copy($transit);
1435 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1437 $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1438 $copy->status( $transit->copy_status );
1439 $self->update_copy();
1440 return if $self->bail_out;
1444 $self->do_hold_notify($hold_transit->hold);
1449 OpenILS::Event->new(
1452 payload => { transit => $transit, holdtransit => $hold_transit } ));
1454 return $hold_transit;
1458 sub checkin_handle_circ {
1462 my $circ = $self->circ;
1463 my $copy = $self->copy;
1467 # backdate the circ if necessary
1468 if($self->backdate) {
1469 $self->checkin_handle_backdate;
1470 return if $self->bail_out;
1473 if(!$circ->stop_fines) {
1474 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1475 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1476 $circ->stop_fines_time('now');
1479 # see if there are any fines owed on this circ. if not, close it
1480 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1481 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1483 # Set the checkin vars since we have the item
1484 $circ->checkin_time('now');
1485 $circ->checkin_staff($self->editor->requestor->id);
1486 $circ->checkin_lib($self->editor->requestor->ws_ou);
1488 my $circ_lib = (ref $self->copy->circ_lib) ?
1489 $self->copy->circ_lib->id : $self->copy->circ_lib;
1490 my $stat = $U->copy_status($self->copy->status)->id;
1492 # If the item is lost/missing and it needs to be sent home, don't
1493 # reshelve the copy, leave it lost/missing so the recipient will know
1494 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1495 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1496 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1499 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1504 return $self->bail_on_events($self->editor->event)
1505 unless $self->editor->update_action_circulation($circ);
1509 sub checkin_handle_backdate {
1512 my $bills = $self->editor->search_money_billing(
1514 billing_ts => { '>=' => $self->backdate },
1515 xact => $self->circ->id,
1516 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1520 for my $bill (@$bills) {
1521 if( !$bill->voided or $bill->voided =~ /f/i ) {
1523 my $n = $bill->note || "";
1524 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1526 $self->bail_on_events($self->editor->event)
1527 unless $self->editor->update_money_billing($bill);
1534 # XXX Legacy version for Circ.pm support
1535 sub _checkin_handle_backdate {
1536 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1538 my $bills = $session->request(
1539 "open-ils.storage.direct.money.billing.search_where.atomic",
1540 billing_ts => { '>=' => $backdate },
1542 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1546 for my $bill (@$bills) {
1548 my $n = $bill->note || "";
1549 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1550 my $s = $session->request(
1551 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1552 return $U->DB_UPDATE_FAILED($bill) unless $s;
1562 sub find_patron_from_copy {
1564 my $circs = $self->editor->search_action_circulation(
1565 { target_copy => $self->copy->id, checkin_time => undef });
1566 my $circ = $circs->[0];
1567 return unless $circ;
1568 my $u = $self->editor->retrieve_actor_user($circ->usr)
1569 or return $self->bail_on_events($self->editor->event);
1573 sub check_checkin_copy_status {
1575 my $copy = $self->copy;
1581 my $status = $U->copy_status($copy->status)->id;
1584 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1585 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1586 $status == OILS_COPY_STATUS_IN_PROCESS ||
1587 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1588 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1589 $status == OILS_COPY_STATUS_CATALOGING ||
1590 $status == OILS_COPY_STATUS_RESHELVING );
1592 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1593 if( $status == OILS_COPY_STATUS_LOST );
1595 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1596 if( $status == OILS_COPY_STATUS_MISSING );
1598 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1603 # --------------------------------------------------------------------------
1604 # On checkin, we need to return as many relevant objects as we can
1605 # --------------------------------------------------------------------------
1606 sub checkin_flesh_events {
1609 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1610 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1611 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1615 for my $evt (@{$self->events}) {
1618 $payload->{copy} = $U->unflesh_copy($self->copy);
1619 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1620 $payload->{circ} = $self->circ;
1621 $payload->{transit} = $self->transit;
1622 $payload->{hold} = $self->hold;
1624 $evt->{payload} = $payload;
1629 my( $self, $msg ) = @_;
1630 my $bc = ($self->copy) ? $self->copy->barcode :
1633 my $usr = ($self->patron) ? $self->patron->id : "";
1634 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1635 ", recipient=$usr, copy=$bc");
1641 $self->log_me("do_renew()");
1642 $self->is_renewal(1);
1644 unless( $self->is_renewal ) {
1645 return $self->bail_on_events($self->editor->events)
1646 unless $self->editor->allowed('RENEW_CIRC');
1649 # Make sure there is an open circ to renew that is not
1650 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1651 my $circ = $self->editor->search_action_circulation(
1652 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1654 return $self->bail_on_events($self->editor->event) unless $circ;
1656 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1657 if $circ->renewal_remaining < 1;
1659 # -----------------------------------------------------------------
1661 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1664 $self->run_renew_permit;
1667 $self->do_checkin();
1668 return if $self->bail_out;
1670 unless( $self->permit_override ) {
1672 return if $self->bail_out;
1673 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1674 $self->remove_event('ITEM_NOT_CATALOGED');
1677 $self->override_events;
1678 return if $self->bail_out;
1681 $self->do_checkout();
1686 my( $self, $evt ) = @_;
1687 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1688 $logger->debug("circulator: removing event from list: $evt");
1689 my @events = @{$self->events};
1690 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1695 my( $self, $evt ) = @_;
1696 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1697 return grep { $_->{textcode} eq $evt } @{$self->events};
1702 sub run_renew_permit {
1704 my $runner = $self->script_runner;
1706 $runner->load($self->circ_permit_renew);
1707 my $result = $runner->run or
1708 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1709 my $events = $result->{events};
1711 $logger->activity("circ_permit_renew for user ".
1712 $self->patron->id." returned events: @$events") if @$events;
1714 $self->push_events(OpenILS::Event->new($_)) for @$events;
1716 $logger->debug("circulator: re-creating script runner to be safe");
1717 $self->mk_script_runner;