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->mk_script_runner;
149 return circ_events($circulator) if $circulator->bail_out;
151 $circulator->circ_permit_patron($scripts{circ_permit_patron});
152 $circulator->circ_permit_copy($scripts{circ_permit_copy});
153 $circulator->circ_duration($scripts{circ_duration});
154 $circulator->circ_permit_renew($scripts{circ_permit_renew});
156 $circulator->override(1) if $api =~ /override/o;
158 if( $api =~ /checkout\.permit/ ) {
159 $circulator->do_permit();
161 } elsif( $api =~ /checkout/ ) {
162 $circulator->do_checkout();
164 } elsif( $api =~ /checkin/ ) {
165 $circulator->do_checkin();
167 } elsif( $api =~ /renew/ ) {
168 $circulator->is_renewal(1);
169 $circulator->do_renew();
172 if( $circulator->bail_out ) {
175 # make sure no success event accidentally slip in
177 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
178 my @e = @{$circulator->events};
179 push( @ee, $_->{textcode} ) for @e;
180 $logger->info("circulator: bailing out with events: @ee");
181 $circulator->editor->xact_rollback;
184 $circulator->editor->commit;
187 $circulator->script_runner->cleanup;
189 return circ_events($circulator);
194 my @e = @{$circ->events};
195 return (@e == 1) ? $e[0] : \@e;
200 sub translate_legacy_args {
203 if( $$args{barcode} ) {
204 $$args{copy_barcode} = $$args{barcode};
205 delete $$args{barcode};
208 if( $$args{copyid} ) {
209 $$args{copy_id} = $$args{copyid};
210 delete $$args{copyid};
213 if( $$args{patronid} ) {
214 $$args{patron_id} = $$args{patronid};
215 delete $$args{patronid};
218 if( $$args{patron} and !ref($$args{patron}) ) {
219 $$args{patron_id} = $$args{patron};
220 delete $$args{patron};
224 if( $$args{noncat} ) {
225 $$args{is_noncat} = $$args{noncat};
226 delete $$args{noncat};
229 if( $$args{precat} ) {
230 $$args{is_precat} = $$args{precat};
231 delete $$args{precat};
237 # --------------------------------------------------------------------------
238 # This package actually manages all of the circulation logic
239 # --------------------------------------------------------------------------
240 package OpenILS::Application::Circ::Circulator;
241 use strict; use warnings;
242 use vars q/$AUTOLOAD/;
244 use OpenILS::Utils::Fieldmapper;
245 use OpenSRF::Utils::Cache;
246 use Digest::MD5 qw(md5_hex);
247 use DateTime::Format::ISO8601;
248 use OpenILS::Utils::PermitHold;
249 use OpenSRF::Utils qw/:datetime/;
250 use OpenSRF::Utils::SettingsClient;
251 use OpenILS::Application::Circ::Holds;
252 use OpenILS::Application::Circ::Transit;
253 use OpenSRF::Utils::Logger qw(:logger);
254 use OpenILS::Utils::CStoreEditor qw/:funcs/;
255 use OpenILS::Application::Circ::ScriptBuilder;
256 use OpenILS::Const qw/:const/;
258 my $U = "OpenILS::Application::AppUtils";
259 my $holdcode = "OpenILS::Application::Circ::Holds";
260 my $transcode = "OpenILS::Application::Circ::Transit";
265 # --------------------------------------------------------------------------
266 # Add a pile of automagic getter/setter methods
267 # --------------------------------------------------------------------------
268 my @AUTOLOAD_FIELDS = qw/
305 recurring_fines_level
322 my $type = ref($self) or die "$self is not an object";
324 my $name = $AUTOLOAD;
327 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
328 $logger->error("$type: invalid autoload field: $name");
329 die "$type: invalid autoload field: $name\n"
334 *{"${type}::${name}"} = sub {
337 $s->{$name} = $v if defined $v;
341 return $self->$name($data);
346 my( $class, $auth, %args ) = @_;
347 $class = ref($class) || $class;
348 my $self = bless( {}, $class );
352 new_editor(xact => 1, authtoken => $auth) );
354 unless( $self->editor->checkauth ) {
355 $self->bail_on_events($self->editor->event);
359 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
361 $self->$_($args{$_}) for keys %args;
364 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
370 # --------------------------------------------------------------------------
371 # True if we should discontinue processing
372 # --------------------------------------------------------------------------
374 my( $self, $bool ) = @_;
375 if( defined $bool ) {
376 $logger->info("circulator: BAILING OUT") if $bool;
377 $self->{bail_out} = $bool;
379 return $self->{bail_out};
384 my( $self, @evts ) = @_;
387 $logger->info("circulator: pushing event ".$e->{textcode});
388 push( @{$self->events}, $e ) unless
389 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
395 my $key = md5_hex( time() . rand() . "$$" );
396 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
397 return $self->permit_key($key);
400 sub check_permit_key {
402 my $key = $self->permit_key;
403 return 0 unless $key;
404 my $k = "oils_permit_key_$key";
405 my $one = $self->cache_handle->get_cache($k);
406 $self->cache_handle->delete_cache($k);
407 return ($one) ? 1 : 0;
411 # --------------------------------------------------------------------------
412 # This builds the script runner environment and fetches most of the
414 # --------------------------------------------------------------------------
415 sub mk_script_runner {
420 qw/copy copy_barcode copy_id patron
421 patron_id patron_barcode volume title editor/;
423 # Translate our objects into the ScriptBuilder args hash
424 $$args{$_} = $self->$_() for @fields;
425 $$args{fetch_patron_by_circ_copy} = 1;
426 $$args{fetch_patron_circ_info} = 1;
428 # This fetches most of the objects we need
429 $self->script_runner(
430 OpenILS::Application::Circ::ScriptBuilder->build($args));
432 # Now we translate the ScriptBuilder objects back into self
433 $self->$_($$args{$_}) for @fields;
435 my @evts = @{$args->{_events}} if $args->{_events};
437 $logger->debug("script builder returned events: : @evts") if @evts;
441 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
442 if(!$self->is_noncat and
444 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
448 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
449 return $self->bail_on_events(@e);
453 $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
455 # Set some circ-specific flags in the script environment
456 my $evt = "environment";
457 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
459 if( $self->is_noncat ) {
460 $self->script_runner->insert("$evt.isNonCat", 1);
461 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
464 $self->script_runner->add_path( $_ ) for @$script_libs;
472 # --------------------------------------------------------------------------
473 # Does the circ permit work
474 # --------------------------------------------------------------------------
478 $self->log_me("do_permit()");
480 unless( $self->editor->requestor->id == $self->patron->id ) {
481 return $self->bail_on_events($self->editor->event)
482 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
485 $self->do_copy_checks();
486 return if $self->bail_out;
487 $self->run_patron_permit_scripts();
488 $self->run_copy_permit_scripts()
489 unless $self->is_precat or $self->is_noncat;
490 $self->override_events() unless $self->is_renewal;
491 return if $self->bail_out;
493 if( $self->is_precat ) {
496 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
497 return $self->bail_out(1) unless $self->is_renewal;
503 payload => $self->mk_permit_key));
509 my $copy = $self->copy;
512 my $stat = $U->copy_status($copy->status)->id;
514 # We cannot check out a copy if it is in-transit
515 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
516 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
519 $self->handle_claims_returned();
520 return if $self->bail_out;
522 # no claims returned circ was found, check if there is any open circ
523 unless( $self->is_renewal ) {
524 my $circs = $self->editor->search_action_circulation(
525 { target_copy => $copy->id, stop_fines_time => undef }
528 return $self->bail_on_events(
529 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
534 # ---------------------------------------------------------------------
535 # This pushes any patron-related events into the list but does not
536 # set bail_out for any events
537 # ---------------------------------------------------------------------
538 sub run_patron_permit_scripts {
540 my $runner = $self->script_runner;
541 my $patronid = $self->patron->id;
543 # ---------------------------------------------------------------------
544 # Find all of the fatal penalties currently set on the user
545 # ---------------------------------------------------------------------
546 my $penalties = $U->update_patron_penalties(
547 authtoken => $self->editor->authtoken,
548 patron => $self->patron,
551 $penalties = $penalties->{fatal_penalties};
553 # ---------------------------------------------------------------------
554 # Now run the patron permit script
555 # ---------------------------------------------------------------------
556 $runner->load($self->circ_permit_patron);
557 my $result = $runner->run or
558 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
560 my $patron_events = $result->{events};
562 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
564 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
566 $self->push_events(@allevents);
570 sub run_copy_permit_scripts {
572 my $copy = $self->copy || return;
573 my $runner = $self->script_runner;
575 # ---------------------------------------------------------------------
576 # Capture all of the copy permit events
577 # ---------------------------------------------------------------------
578 $runner->load($self->circ_permit_copy);
579 my $result = $runner->run or
580 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
581 my $copy_events = $result->{events};
583 # ---------------------------------------------------------------------
584 # Now collect all of the events together
585 # ---------------------------------------------------------------------
587 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
589 # See if this copy has an alert message
590 my $ae = $self->check_copy_alert();
591 push( @allevents, $ae ) if $ae;
593 # uniquify the events
594 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
595 @allevents = values %hash;
598 $_->{payload} = $copy if
599 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
602 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
604 $self->push_events(@allevents);
608 sub check_copy_alert {
610 return OpenILS::Event->new(
611 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
612 if $self->copy and $self->copy->alert_message;
618 # --------------------------------------------------------------------------
619 # If the call is overriding and has permissions to override every collected
620 # event, the are cleared. Any event that the caller does not have
621 # permission to override, will be left in the event list and bail_out will
623 # XXX We need code in here to cancel any holds/transits on copies
624 # that are being force-checked out
625 # --------------------------------------------------------------------------
626 sub override_events {
628 my @events = @{$self->events};
629 return unless @events;
631 if(!$self->override) {
632 return $self->bail_out(1)
633 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
638 for my $e (@events) {
639 my $tc = $e->{textcode};
640 next if $tc eq 'SUCCESS';
641 my $ov = "$tc.override";
642 $logger->info("circulator: attempting to override event: $ov");
644 return $self->bail_on_events($self->editor->event)
645 unless( $self->editor->allowed($ov) );
650 # --------------------------------------------------------------------------
651 # If there is an open claimsreturn circ on the requested copy, close the
652 # circ if overriding, otherwise bail out
653 # --------------------------------------------------------------------------
654 sub handle_claims_returned {
656 my $copy = $self->copy;
658 my $CR = $self->editor->search_action_circulation(
660 target_copy => $copy->id,
661 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
662 checkin_time => undef,
666 return unless ($CR = $CR->[0]);
670 # - If the caller has set the override flag, we will check the item in
671 if($self->override) {
673 $CR->checkin_time('now');
674 $CR->checkin_lib($self->editor->requestor->ws_ou);
675 $CR->checkin_staff($self->editor->requestor->id);
677 $evt = $self->editor->event
678 unless $self->editor->update_action_circulation($CR);
681 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
684 $self->bail_on_events($evt) if $evt;
689 # --------------------------------------------------------------------------
690 # This performs the checkout
691 # --------------------------------------------------------------------------
695 $self->log_me("do_checkout()");
697 # make sure perms are good if this isn't a renewal
698 unless( $self->is_renewal ) {
699 return $self->bail_on_events($self->editor->event)
700 unless( $self->editor->allowed('COPY_CHECKOUT') );
703 # verify the permit key
704 unless( $self->check_permit_key ) {
705 if( $self->permit_override ) {
706 return $self->bail_on_events($self->editor->event)
707 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
709 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
713 # if this is a non-cataloged circ, build the circ and finish
714 if( $self->is_noncat ) {
715 $self->checkout_noncat;
717 OpenILS::Event->new('SUCCESS',
718 payload => { noncat_circ => $self->circ }));
722 if( $self->is_precat ) {
723 $self->script_runner->insert("environment.isPrecat", 1, 1);
724 $self->make_precat_copy;
725 return if $self->bail_out;
727 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
728 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
731 $self->do_copy_checks;
732 return if $self->bail_out;
734 $self->run_checkout_scripts();
735 return if $self->bail_out;
737 $self->build_checkout_circ_object();
738 return if $self->bail_out;
740 $self->apply_modified_due_date();
741 return if $self->bail_out;
743 return $self->bail_on_events($self->editor->event)
744 unless $self->editor->create_action_circulation($self->circ);
746 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
748 return if $self->bail_out;
750 $self->handle_checkout_holds();
751 return if $self->bail_out;
753 # ------------------------------------------------------------------------------
754 # Update the patron penalty info in the DB
755 # ------------------------------------------------------------------------------
756 $U->update_patron_penalties(
757 authtoken => $self->editor->authtoken,
758 patron => $self->patron,
762 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
764 OpenILS::Event->new('SUCCESS',
766 copy => $U->unflesh_copy($self->copy),
769 holds_fulfilled => $self->fulfilled_holds,
777 my $copy = $self->copy;
779 my $stat = $copy->status if ref $copy->status;
780 my $loc = $copy->location if ref $copy->location;
781 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
783 $copy->status($stat->id) if $stat;
784 $copy->location($loc->id) if $loc;
785 $copy->circ_lib($circ_lib->id) if $circ_lib;
786 $copy->editor($self->editor->requestor->id);
787 $copy->edit_date('now');
789 return $self->bail_on_events($self->editor->event)
790 unless $self->editor->update_asset_copy($self->copy);
792 $copy->status($U->copy_status($copy->status));
793 $copy->location($loc) if $loc;
794 $copy->circ_lib($circ_lib) if $circ_lib;
799 my( $self, @evts ) = @_;
800 $self->push_events(@evts);
804 sub handle_checkout_holds {
807 my $copy = $self->copy;
808 my $patron = $self->patron;
810 my $holds = $self->editor->search_action_hold_request(
812 current_copy => $copy->id ,
813 cancel_time => undef,
814 fulfillment_time => undef
820 # XXX We should only fulfill one hold here...
821 # XXX If a hold was transited to the user who is checking out
822 # the item, we need to make sure that hold is what's grabbed
825 # for now, just sort by id to get what should be the oldest hold
826 $holds = [ sort { $a->id <=> $b->id } @$holds ];
827 my @myholds = grep { $_->usr eq $patron->id } @$holds;
828 my @altholds = grep { $_->usr ne $patron->id } @$holds;
831 my $hold = $myholds[0];
833 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
835 # if the hold was never officially captured, capture it.
836 $hold->capture_time('now') unless $hold->capture_time;
838 # just make sure it's set correctly
839 $hold->current_copy($copy->id);
841 $hold->fulfillment_time('now');
842 $hold->fulfillment_staff($self->editor->requestor->id);
843 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
845 return $self->bail_on_events($self->editor->event)
846 unless $self->editor->update_action_hold_request($hold);
848 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
850 push( @fulfilled, $hold->id );
853 # If there are any holds placed for other users that point to this copy,
854 # then we need to un-target those holds so the targeter can pick a new copy
857 $logger->info("circulator: un-targeting hold ".$_->id.
858 " because copy ".$copy->id." is getting checked out");
860 # - make the targeter process this hold at next run
861 $_->clear_prev_check_time;
863 # - clear out the targetted copy
864 $_->clear_current_copy;
865 $_->clear_capture_time;
867 return $self->bail_on_event($self->editor->event)
868 unless $self->editor->update_action_hold_request($_);
872 $self->fulfilled_holds(\@fulfilled);
877 sub run_checkout_scripts {
881 my $runner = $self->script_runner;
882 $runner->load($self->circ_duration);
884 my $result = $runner->run or
885 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
887 my $duration = $result->{durationRule};
888 my $recurring = $result->{recurringFinesRule};
889 my $max_fine = $result->{maxFine};
891 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
893 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
894 return $self->bail_on_events($evt) if $evt;
896 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
897 return $self->bail_on_events($evt) if $evt;
899 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
900 return $self->bail_on_events($evt) if $evt;
904 # The item circulates with an unlimited duration
910 $self->duration_rule($duration);
911 $self->recurring_fines_rule($recurring);
912 $self->max_fine_rule($max_fine);
916 sub build_checkout_circ_object {
919 my $circ = Fieldmapper::action::circulation->new;
920 my $duration = $self->duration_rule;
921 my $max = $self->max_fine_rule;
922 my $recurring = $self->recurring_fines_rule;
923 my $copy = $self->copy;
924 my $patron = $self->patron;
928 my $dname = $duration->name;
929 my $mname = $max->name;
930 my $rname = $recurring->name;
932 $logger->debug("circulator: building circulation ".
933 "with duration=$dname, maxfine=$mname, recurring=$rname");
935 $circ->duration( $duration->shrt )
936 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
937 $circ->duration( $duration->normal )
938 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
939 $circ->duration( $duration->extended )
940 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
942 $circ->recuring_fine( $recurring->low )
943 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
944 $circ->recuring_fine( $recurring->normal )
945 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
946 $circ->recuring_fine( $recurring->high )
947 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
949 $circ->duration_rule( $duration->name );
950 $circ->recuring_fine_rule( $recurring->name );
951 $circ->max_fine_rule( $max->name );
952 $circ->max_fine( $max->amount );
954 $circ->fine_interval($recurring->recurance_interval);
955 $circ->renewal_remaining( $duration->max_renewals );
959 $logger->info("circulator: copy found with an unlimited circ duration");
960 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
961 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
962 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
963 $circ->renewal_remaining(0);
966 $circ->target_copy( $copy->id );
967 $circ->usr( $patron->id );
968 $circ->circ_lib( $self->circ_lib );
970 if( $self->is_renewal ) {
971 $circ->opac_renewal(1);
972 $circ->renewal_remaining($self->renewal_remaining);
973 $circ->circ_staff($self->editor->requestor->id);
976 # if the user provided an overiding checkout time,
977 # (e.g. the checkout really happened several hours ago), then
978 # we apply that here. Does this need a perm??
979 $circ->xact_start(clense_ISO8601($self->checkout_time))
980 if $self->checkout_time;
982 # if a patron is renewing, 'requestor' will be the patron
983 $circ->circ_staff($self->editor->requestor->id);
984 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
990 sub apply_modified_due_date {
992 my $circ = $self->circ;
993 my $copy = $self->copy;
995 if( $self->due_date ) {
997 return $self->bail_on_events($self->editor->event)
998 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1000 $circ->due_date(clense_ISO8601($self->due_date));
1004 # if the due_date lands on a day when the location is closed
1005 return unless $copy and $circ->due_date;
1007 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1009 $logger->info("circ searching for closed date overlap on lib $org".
1010 " with an item due date of ".$circ->due_date );
1012 my $dateinfo = $U->storagereq(
1013 'open-ils.storage.actor.org_unit.closed_date.overlap',
1014 $org, $circ->due_date );
1017 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1018 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1020 # XXX make the behavior more dynamic
1021 # for now, we just push the due date to after the close date
1022 $circ->due_date($dateinfo->{end});
1029 sub create_due_date {
1030 my( $self, $duration ) = @_;
1031 my ($sec,$min,$hour,$mday,$mon,$year) =
1032 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1033 $year += 1900; $mon += 1;
1034 my $due_date = sprintf(
1035 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1036 $year, $mon, $mday, $hour, $min, $sec);
1042 sub make_precat_copy {
1044 my $copy = $self->copy;
1047 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1049 $copy->editor($self->editor->requestor->id);
1050 $copy->edit_date('now');
1051 $copy->dummy_title($self->dummy_title);
1052 $copy->dummy_author($self->dummy_author);
1054 $self->update_copy();
1058 $logger->info("circulator: Creating a new precataloged ".
1059 "copy in checkout with barcode " . $self->copy_barcode);
1061 $copy = Fieldmapper::asset::copy->new;
1062 $copy->circ_lib($self->circ_lib);
1063 $copy->creator($self->editor->requestor->id);
1064 $copy->editor($self->editor->requestor->id);
1065 $copy->barcode($self->copy_barcode);
1066 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1067 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1068 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1070 $copy->dummy_title($self->dummy_title || "");
1071 $copy->dummy_author($self->dummy_author || "");
1073 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1075 $self->push_events($self->editor->event);
1079 # this is a little bit of a hack, but we need to
1080 # get the copy into the script runner
1081 $self->script_runner->insert("environment.copy", $copy, 1);
1085 sub checkout_noncat {
1091 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1092 my $count = $self->noncat_count || 1;
1093 my $cotime = clense_ISO8601($self->checkout_time) || "";
1095 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1099 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1100 $self->editor->requestor->id,
1108 $self->push_events($evt);
1119 $self->log_me("do_checkin()");
1121 return $self->bail_on_events(
1122 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1125 unless( $self->is_renewal ) {
1126 return $self->bail_on_events($self->editor->event)
1127 unless $self->editor->allowed('COPY_CHECKIN');
1130 $self->push_events($self->check_copy_alert());
1131 $self->push_events($self->check_checkin_copy_status());
1134 # the renew code will have already found our circulation object
1135 unless( $self->is_renewal and $self->circ ) {
1137 # first lets see if we have a good old fashioned open circulation
1138 my $circ = $self->editor->search_action_circulation(
1139 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1142 # if not, lets look for other circs we can check in
1143 $circ = $self->editor->search_action_circulation(
1145 target_copy => $self->copy->id,
1146 xact_finish => undef,
1148 OILS_STOP_FINES_CLAIMSRETURNED,
1149 OILS_STOP_FINES_LOST,
1150 OILS_STOP_FINES_LONGOVERDUE,
1159 # if the circ is marked as 'claims returned', add the event to the list
1160 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1161 if ($self->circ and $self->circ->stop_fines
1162 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1164 # handle the overridable events
1165 $self->override_events unless $self->is_renewal;
1166 return if $self->bail_out;
1170 $self->editor->search_action_transit_copy(
1171 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1175 $self->checkin_handle_circ;
1176 return if $self->bail_out;
1177 $self->checkin_changed(1);
1179 } elsif( $self->transit ) {
1180 my $hold_transit = $self->process_received_transit;
1181 $self->checkin_changed(1);
1183 if( $self->bail_out ) {
1184 $self->checkin_flesh_events;
1188 if( my $e = $self->check_checkin_copy_status() ) {
1189 # If the original copy status is special, alert the caller
1190 return $self->bail_on_events($e);
1194 if( $hold_transit or
1195 $U->copy_status($self->copy->status)->id
1196 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1197 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1198 $self->checkin_flesh_events;
1203 if( $self->is_renewal ) {
1204 $self->push_events(OpenILS::Event->new('SUCCESS'));
1208 # ------------------------------------------------------------------------------
1209 # Circulations and transits are now closed where necessary. Now go on to see if
1210 # this copy can fulfill a hold or needs to be routed to a different location
1211 # ------------------------------------------------------------------------------
1213 if( $self->attempt_checkin_hold_capture() ) {
1214 return if $self->bail_out;
1216 } else { # not needed for a hold
1218 my $circ_lib = (ref $self->copy->circ_lib) ?
1219 $self->copy->circ_lib->id : $self->copy->circ_lib;
1221 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1223 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1225 $self->checkin_handle_precat();
1226 return if $self->bail_out;
1230 $self->checkin_build_copy_transit();
1231 return if $self->bail_out;
1232 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1237 $self->reshelve_copy;
1238 return if $self->bail_out;
1240 unless($self->checkin_changed) {
1242 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1243 my $stat = $U->copy_status($self->copy->status)->id;
1245 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1246 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1247 $self->bail_out(1); # no need to commit anything
1250 $self->push_events(OpenILS::Event->new('SUCCESS'))
1251 unless @{$self->events};
1254 $self->checkin_flesh_events;
1260 my $copy = $self->copy;
1261 my $force = $self->force;
1263 my $stat = $U->copy_status($copy->status)->id;
1266 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1267 $stat != OILS_COPY_STATUS_AVAILABLE and
1268 $stat != OILS_COPY_STATUS_CATALOGING and
1269 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1270 $stat != OILS_COPY_STATUS_RESHELVING )) {
1272 $copy->status( OILS_COPY_STATUS_RESHELVING );
1274 $self->checkin_changed(1);
1279 sub checkin_handle_precat {
1281 my $copy = $self->copy;
1283 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1284 $copy->status(OILS_COPY_STATUS_CATALOGING);
1285 $self->update_copy();
1286 $self->checkin_changed(1);
1287 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1292 sub checkin_build_copy_transit {
1294 my $copy = $self->copy;
1295 my $transit = Fieldmapper::action::transit_copy->new;
1297 $transit->source($self->editor->requestor->ws_ou);
1298 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1299 $transit->target_copy($copy->id);
1300 $transit->source_send_time('now');
1301 $transit->copy_status( $U->copy_status($copy->status)->id );
1303 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1305 return $self->bail_on_events($self->editor->event)
1306 unless $self->editor->create_action_transit_copy($transit);
1308 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1310 $self->checkin_changed(1);
1314 sub attempt_checkin_hold_capture {
1316 my $copy = $self->copy;
1318 # See if this copy can fulfill any holds
1319 my ($hold) = $holdcode->find_nearest_permitted_hold(
1320 OpenSRF::AppSession->create('open-ils.storage'),
1321 $copy, $self->editor->requestor );
1324 $logger->debug("circulator: no potential permitted".
1325 "holds found for copy ".$copy->barcode);
1329 $logger->info("circulator: found permitted hold ".
1330 $hold->id . " for copy, capturing...");
1332 $hold->current_copy($copy->id);
1333 $hold->capture_time('now');
1335 # prevent DB errors caused by fetching
1336 # holds from storage, and updating through cstore
1337 $hold->clear_fulfillment_time;
1338 $hold->clear_fulfillment_staff;
1339 $hold->clear_fulfillment_lib;
1340 $hold->clear_expire_time;
1341 $hold->clear_cancel_time;
1343 $self->bail_on_events($self->editor->event)
1344 unless $self->editor->update_action_hold_request($hold);
1346 $self->checkin_changed(1);
1348 return 1 if $self->bail_out;
1350 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1352 # This hold was captured in the correct location
1353 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1354 $self->push_events(OpenILS::Event->new('SUCCESS'));
1356 # my $evt = $holdcode->hold_email_notifify(
1357 # $self->editor, $hold, $self->title, $self->volume, $self->copy );
1358 # $self->bail_on_events($evt) if $evt;
1362 # Hold needs to be picked up elsewhere. Build a hold
1363 # transit and route the item.
1364 $self->checkin_build_hold_transit();
1365 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1366 return 1 if $self->bail_out;
1368 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1371 # make sure we save the copy status
1377 sub checkin_build_hold_transit {
1381 my $copy = $self->copy;
1382 my $hold = $self->hold;
1383 my $trans = Fieldmapper::action::hold_transit_copy->new;
1385 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1387 $trans->hold($hold->id);
1388 $trans->source($self->editor->requestor->ws_ou);
1389 $trans->dest($hold->pickup_lib);
1390 $trans->source_send_time("now");
1391 $trans->target_copy($copy->id);
1393 # when the copy gets to its destination, it will recover
1394 # this status - put it onto the holds shelf
1395 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1397 return $self->bail_on_events($self->editor->event)
1398 unless $self->editor->create_action_hold_transit_copy($trans);
1403 sub process_received_transit {
1405 my $copy = $self->copy;
1406 my $copyid = $self->copy->id;
1408 my $status_name = $U->copy_status($copy->status)->name;
1409 $logger->debug("circulator: attempting transit receive on ".
1410 "copy $copyid. Copy status is $status_name");
1412 my $transit = $self->transit;
1414 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1415 $logger->activity("Fowarding transit on copy which is destined ".
1416 "for a different location. copy=$copyid,current ".
1417 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1419 $self->bail_on_events(
1420 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1423 # The transit is received, set the receive time
1424 $transit->dest_recv_time('now');
1425 $self->bail_on_events($self->editor->event)
1426 unless $self->editor->update_action_transit_copy($transit);
1428 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1430 $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1431 $copy->status( $transit->copy_status );
1432 $self->update_copy();
1433 return if $self->bail_out;
1435 my $ishold = ($hold_transit) ? 1 : 0;
1438 OpenILS::Event->new(
1441 payload => { transit => $transit, holdtransit => $hold_transit } ));
1443 return $hold_transit;
1447 sub checkin_handle_circ {
1451 my $circ = $self->circ;
1452 my $copy = $self->copy;
1456 # backdate the circ if necessary
1457 if($self->backdate) {
1458 $self->checkin_handle_backdate;
1459 return if $self->bail_out;
1462 if(!$circ->stop_fines) {
1463 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1464 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1465 $circ->stop_fines_time('now');
1468 # see if there are any fines owed on this circ. if not, close it
1469 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1470 $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1472 # Set the checkin vars since we have the item
1473 $circ->checkin_time('now');
1474 $circ->checkin_staff($self->editor->requestor->id);
1475 $circ->checkin_lib($self->editor->requestor->ws_ou);
1477 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1480 return $self->bail_on_events($self->editor->event)
1481 unless $self->editor->update_action_circulation($circ);
1485 sub checkin_handle_backdate {
1488 my $bills = $self->editor->search_money_billing(
1489 { billing_ts => { ">=" => $self->backdate }, "xact" => $self->circ->id }
1492 for my $bill (@$bills) {
1493 if( !$bill->voided or $bill->voided =~ /f/i ) {
1495 my $n = $bill->note || "";
1496 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1498 $self->bail_on_events($self->editor->event)
1499 unless $self->editor->update_money_billing($bill);
1506 # XXX Legacy version for Circ.pm support
1507 sub _checkin_handle_backdate {
1508 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1510 my $bills = $session->request(
1511 "open-ils.storage.direct.money.billing.search_where.atomic",
1512 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1515 for my $bill (@$bills) {
1517 my $n = $bill->note || "";
1518 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1519 my $s = $session->request(
1520 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1521 return $U->DB_UPDATE_FAILED($bill) unless $s;
1531 sub find_patron_from_copy {
1533 my $circs = $self->editor->search_action_circulation(
1534 { target_copy => $self->copy->id, stop_fines_time => undef });
1535 my $circ = $circs->[0];
1536 return unless $circ;
1537 my $u = $self->editor->retrieve_actor_user($circ->usr)
1538 or return $self->bail_on_events($self->editor->event);
1542 sub check_checkin_copy_status {
1544 my $copy = $self->copy;
1550 my $status = $U->copy_status($copy->status)->id;
1553 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1554 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1555 $status == OILS_COPY_STATUS_IN_PROCESS ||
1556 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1557 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1558 $status == OILS_COPY_STATUS_RESHELVING );
1560 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1561 if( $status == OILS_COPY_STATUS_LOST );
1563 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1564 if( $status == OILS_COPY_STATUS_MISSING );
1566 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1571 # --------------------------------------------------------------------------
1572 # On checkin, we need to return as many relevant objects as we can
1573 # --------------------------------------------------------------------------
1574 sub checkin_flesh_events {
1577 for my $evt (@{$self->events}) {
1580 $payload->{copy} = $U->unflesh_copy($self->copy);
1581 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1582 $payload->{circ} = $self->circ;
1583 $payload->{transit} = $self->transit;
1584 $payload->{hold} = $self->hold;
1586 $evt->{payload} = $payload;
1591 my( $self, $msg ) = @_;
1592 my $bc = ($self->copy) ? $self->copy->barcode :
1595 my $usr = ($self->patron) ? $self->patron->id : "";
1596 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1597 ", recipient=$usr, copy=$bc");
1603 $self->log_me("do_renew()");
1604 $self->is_renewal(1);
1606 unless( $self->is_renewal ) {
1607 return $self->bail_on_events($self->editor->events)
1608 unless $self->editor->allowed('RENEW_CIRC');
1611 # Make sure there is an open circ to renew that is not
1612 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1613 my $circ = $self->editor->search_action_circulation(
1614 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1616 return $self->bail_on_events($self->editor->event) unless $circ;
1618 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1619 if $circ->renewal_remaining < 1;
1621 # -----------------------------------------------------------------
1623 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1624 $self->renewal_remaining(0) if $self->renewal_remaining < 0;
1627 $self->run_renew_permit;
1630 $self->do_checkin();
1631 return if $self->bail_out;
1633 unless( $self->permit_override ) {
1635 return if $self->bail_out;
1636 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1637 $self->remove_event('ITEM_NOT_CATALOGED');
1640 $self->override_events;
1641 return if $self->bail_out;
1644 $self->do_checkout();
1649 my( $self, $evt ) = @_;
1650 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1651 $logger->debug("circulator: removing event from list: $evt");
1652 my @events = @{$self->events};
1653 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1658 my( $self, $evt ) = @_;
1659 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1660 return grep { $_->{textcode} eq $evt } @{$self->events};
1665 sub run_renew_permit {
1667 my $runner = $self->script_runner;
1669 $runner->load($self->circ_permit_renew);
1670 my $result = $runner->run or
1671 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1672 my $events = $result->{events};
1674 $logger->activity("circ_permit_renew for user ".
1675 $self->patron->id." returned events: @$events") if @$events;
1677 $self->push_events(OpenILS::Event->new($_)) for @$events;