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::Application::Circ::Circulator;
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;
257 sub PRECAT_FINE_LEVEL { return 2; }
258 sub PRECAT_LOAN_DURATION { return 2; }
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/
306 recurring_fines_level
323 my $type = ref($self) or die "$self is not an object";
325 my $name = $AUTOLOAD;
328 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
329 $logger->error("$type: invalid autoload field: $name");
330 die "$type: invalid autoload field: $name\n"
335 *{"${type}::${name}"} = sub {
338 $s->{$name} = $v if defined $v;
342 return $self->$name($data);
347 my( $class, $auth, %args ) = @_;
348 $class = ref($class) || $class;
349 my $self = bless( {}, $class );
353 new_editor(xact => 1, authtoken => $auth) );
355 unless( $self->editor->checkauth ) {
356 $self->bail_on_events($self->editor->event);
360 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
362 $self->$_($args{$_}) for keys %args;
365 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
371 # --------------------------------------------------------------------------
372 # True if we should discontinue processing
373 # --------------------------------------------------------------------------
375 my( $self, $bool ) = @_;
376 if( defined $bool ) {
377 $logger->info("circulator: BAILING OUT") if $bool;
378 $self->{bail_out} = $bool;
380 return $self->{bail_out};
385 my( $self, @evts ) = @_;
388 $logger->info("circulator: pushing event ".$e->{textcode});
389 push( @{$self->events}, $e ) unless
390 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
396 my $key = md5_hex( time() . rand() . "$$" );
397 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
398 return $self->permit_key($key);
401 sub check_permit_key {
403 my $key = $self->permit_key;
404 return 0 unless $key;
405 my $k = "oils_permit_key_$key";
406 my $one = $self->cache_handle->get_cache($k);
407 $self->cache_handle->delete_cache($k);
408 return ($one) ? 1 : 0;
412 # --------------------------------------------------------------------------
413 # This builds the script runner environment and fetches most of the
415 # --------------------------------------------------------------------------
416 sub mk_script_runner {
421 qw/copy copy_barcode copy_id patron
422 patron_id patron_barcode volume title editor/;
424 # Translate our objects into the ScriptBuilder args hash
425 $$args{$_} = $self->$_() for @fields;
426 $$args{fetch_patron_by_circ_copy} = 1;
427 $$args{fetch_patron_circ_info} = 1;
429 # This fetches most of the objects we need
430 $self->script_runner(
431 OpenILS::Application::Circ::ScriptBuilder->build($args));
433 # Now we translate the ScriptBuilder objects back into self
434 $self->$_($$args{$_}) for @fields;
436 my @evts = @{$args->{_events}} if $args->{_events};
438 $logger->debug("script builder returned events: : @evts") if @evts;
442 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
443 if(!$self->is_noncat and
445 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
449 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
450 return $self->bail_on_events(@e);
454 $self->is_precat(1) if $self->copy and $self->copy->call_number == -1;
456 # Set some circ-specific flags in the script environment
457 my $evt = "environment";
458 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
460 if( $self->is_noncat ) {
461 $self->script_runner->insert("$evt.isNonCat", 1);
462 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
465 $self->script_runner->add_path( $_ ) for @$script_libs;
473 # --------------------------------------------------------------------------
474 # Does the circ permit work
475 # --------------------------------------------------------------------------
479 $self->log_me("do_permit()");
481 unless( $self->editor->requestor->id == $self->patron->id ) {
482 return $self->bail_on_events($self->editor->event)
483 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
486 $self->do_copy_checks();
487 return if $self->bail_out;
488 $self->run_patron_permit_scripts();
489 $self->run_copy_permit_scripts()
490 unless $self->is_precat or $self->is_noncat;
491 $self->override_events() unless $self->is_renewal;
492 return if $self->bail_out;
494 if( $self->is_precat ) {
497 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
498 return $self->bail_out(1) unless $self->is_renewal;
504 payload => $self->mk_permit_key));
510 my $copy = $self->copy;
513 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
515 # We cannot check out a copy if it is in-transit
516 if( $stat == $U->copy_status_from_name('in transit')->id ) {
517 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
520 $self->handle_claims_returned();
521 return if $self->bail_out;
523 # no claims returned circ was found, check if there is any open circ
524 unless( $self->is_renewal ) {
525 my $circs = $self->editor->search_action_circulation(
526 { target_copy => $copy->id, stop_fines_time => undef }
529 return $self->bail_on_events(
530 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
535 # ---------------------------------------------------------------------
536 # This pushes any patron-related events into the list but does not
537 # set bail_out for any events
538 # ---------------------------------------------------------------------
539 sub run_patron_permit_scripts {
541 my $runner = $self->script_runner;
542 my $patronid = $self->patron->id;
544 # ---------------------------------------------------------------------
545 # Find all of the fatal penalties currently set on the user
546 # ---------------------------------------------------------------------
547 my $penalties = $U->update_patron_penalties(
548 authtoken => $self->editor->authtoken,
549 patron => $self->patron,
552 $penalties = $penalties->{fatal_penalties};
554 # ---------------------------------------------------------------------
555 # Now run the patron permit script
556 # ---------------------------------------------------------------------
557 $runner->load($self->circ_permit_patron);
558 my $result = $runner->run or
559 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
561 my $patron_events = $result->{events};
563 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
565 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
567 $self->push_events(@allevents);
571 sub run_copy_permit_scripts {
573 my $copy = $self->copy || return;
574 my $runner = $self->script_runner;
576 # ---------------------------------------------------------------------
577 # Capture all of the copy permit events
578 # ---------------------------------------------------------------------
579 $runner->load($self->circ_permit_copy);
580 my $result = $runner->run or
581 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
582 my $copy_events = $result->{events};
584 # ---------------------------------------------------------------------
585 # Now collect all of the events together
586 # ---------------------------------------------------------------------
588 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
590 # See if this copy has an alert message
591 my $ae = $self->check_copy_alert();
592 push( @allevents, $ae ) if $ae;
594 # uniquify the events
595 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
596 @allevents = values %hash;
599 # If the script says the copy is not available, put the status
600 # in as the payload for that event
601 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
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 => '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 == -1 ) {
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($U->copy_status_from_name('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;
792 return $self->bail_on_events($self->editor->event)
793 unless $self->editor->update_asset_copy($self->copy);
795 $copy->status($stat) if $stat;
796 $copy->location($loc) if $loc;
797 $copy->circ_lib($circ_lib) if $circ_lib;
802 my( $self, @evts ) = @_;
803 $self->push_events(@evts);
807 sub handle_checkout_holds {
810 my $copy = $self->copy;
811 my $patron = $self->patron;
813 my $holds = $self->editor->search_action_hold_request(
815 current_copy => $copy->id ,
816 cancel_time => undef,
817 fulfillment_time => undef
823 # XXX We should only fulfill one hold here...
824 # XXX If a hold was transited to the user who is checking out
825 # the item, we need to make sure that hold is what's grabbed
828 # for now, just sort by id to get what should be the oldest hold
829 $holds = [ sort { $a->id <=> $b->id } @$holds ];
830 my @myholds = grep { $_->usr eq $patron->id } @$holds;
831 my @altholds = grep { $_->usr ne $patron->id } @$holds;
834 my $hold = $myholds[0];
836 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
838 # if the hold was never officially captured, capture it.
839 $hold->capture_time('now') unless $hold->capture_time;
841 # just make sure it's set correctly
842 $hold->current_copy($copy->id);
844 $hold->fulfillment_time('now');
845 $hold->fulfillment_staff($self->editor->requestor->id);
846 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
848 return $self->bail_on_events($self->editor->event)
849 unless $self->editor->update_action_hold_request($hold);
851 push( @fulfilled, $hold->id );
854 # If there are any holds placed for other users that point to this copy,
855 # then we need to un-target those holds so the targeter can pick a new copy
858 $logger->info("circulator: un-targeting hold ".$_->id.
859 " because copy ".$copy->id." is getting checked out");
861 # - make the targeter process this hold at next run
862 $_->clear_prev_check_time;
864 # - clear out the targetted copy
865 $_->clear_current_copy;
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 $dur_level = $result->{durationLevel};
889 my $recurring = $result->{recurringFinesRule};
890 my $max_fine = $result->{maxFine};
891 my $rec_fines_level = $result->{recurringFinesLevel};
893 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
894 return $self->bail_on_events($evt) if $evt;
895 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
896 return $self->bail_on_events($evt) if $evt;
897 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
898 return $self->bail_on_events($evt) if $evt;
900 $self->duration_level($dur_level);
901 $self->recurring_fines_level($rec_fines_level);
902 $self->duration_rule($duration);
903 $self->recurring_fines_rule($recurring);
904 $self->max_fine_rule($max_fine);
908 sub build_checkout_circ_object {
911 my $circ = Fieldmapper::action::circulation->new;
912 my $duration = $self->duration_rule;
913 my $max = $self->max_fine_rule;
914 my $recurring = $self->recurring_fines_rule;
915 my $copy = $self->copy;
916 my $patron = $self->patron;
917 my $dur_level = $self->duration_level;
918 my $rec_level = $self->recurring_fines_level;
920 my $dname = $duration->name;
921 my $mname = $max->name;
922 my $rname = $recurring->name;
924 $logger->debug("circulator: building circulation with duration=$dname, ".
925 "maxfine=$mname, recurring=$rname, duration-level=$dur_level, recurring-level=$rec_level");
927 $circ->duration( $duration->shrt ) if ($dur_level == 1);
928 $circ->duration( $duration->normal ) if ($dur_level == 2);
929 $circ->duration( $duration->extended ) if ($dur_level == 3);
931 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
932 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
933 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
935 $circ->duration_rule( $duration->name );
936 $circ->recuring_fine_rule( $recurring->name );
937 $circ->max_fine_rule( $max->name );
938 $circ->max_fine( $max->amount );
940 $circ->fine_interval($recurring->recurance_interval);
941 $circ->renewal_remaining( $duration->max_renewals );
942 $circ->target_copy( $copy->id );
943 $circ->usr( $patron->id );
944 $circ->circ_lib( $self->circ_lib );
946 if( $self->is_renewal ) {
947 $circ->opac_renewal(1);
948 $circ->renewal_remaining($self->renewal_remaining);
949 $circ->circ_staff($self->editor->requestor->id);
953 # if the user provided an overiding checkout time,
954 # (e.g. the checkout really happened several hours ago), then
955 # we apply that here. Does this need a perm??
956 $circ->xact_start(clense_ISO8601($self->checkout_time))
957 if $self->checkout_time;
959 # if a patron is renewing, 'requestor' will be the patron
960 $circ->circ_staff($self->editor->requestor->id);
961 $circ->due_date( $self->create_due_date($circ->duration) );
967 sub apply_modified_due_date {
969 my $circ = $self->circ;
970 my $copy = $self->copy;
972 if( $self->due_date ) {
974 return $self->bail_on_events($self->editor->event)
975 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
977 $circ->due_date(clense_ISO8601($self->due_date));
981 # if the due_date lands on a day when the location is closed
984 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
986 $logger->info("circ searching for closed date overlap on lib $org".
987 " with an item due date of ".$circ->due_date );
989 my $dateinfo = $U->storagereq(
990 'open-ils.storage.actor.org_unit.closed_date.overlap',
991 $org, $circ->due_date );
994 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
995 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
997 # XXX make the behavior more dynamic
998 # for now, we just push the due date to after the close date
999 $circ->due_date($dateinfo->{end});
1006 sub create_due_date {
1007 my( $self, $duration ) = @_;
1008 my ($sec,$min,$hour,$mday,$mon,$year) =
1009 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1010 $year += 1900; $mon += 1;
1011 my $due_date = sprintf(
1012 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1013 $year, $mon, $mday, $hour, $min, $sec);
1019 sub make_precat_copy {
1021 my $copy = $self->copy;
1024 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1026 $copy->editor($self->editor->requestor->id);
1027 $copy->edit_date('now');
1028 $copy->dummy_title($self->dummy_title);
1029 $copy->dummy_author($self->dummy_author);
1031 $self->update_copy();
1035 $logger->info("circulator: Creating a new precataloged ".
1036 "copy in checkout with barcode " . $self->copy_barcode);
1038 $copy = Fieldmapper::asset::copy->new;
1039 $copy->circ_lib($self->circ_lib);
1040 $copy->creator($self->editor->requestor->id);
1041 $copy->editor($self->editor->requestor->id);
1042 $copy->barcode($self->copy_barcode);
1043 $copy->call_number(-1); #special CN for precat materials
1044 $copy->loan_duration(&PRECAT_LOAN_DURATION);
1045 $copy->fine_level(&PRECAT_FINE_LEVEL);
1047 $copy->dummy_title($self->dummy_title || "");
1048 $copy->dummy_author($self->dummy_author || "");
1050 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1052 $self->push_events($self->editor->event);
1056 # this is a little bit of a hack, but we need to
1057 # get the copy into the script runner
1058 $self->script_runner->insert("environment.copy", $copy, 1);
1062 sub checkout_noncat {
1068 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1069 my $count = $self->noncat_count || 1;
1070 my $cotime = clense_ISO8601($self->checkout_time) || "";
1072 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1076 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1077 $self->editor->requestor->id,
1085 $self->push_events($evt);
1096 $self->log_me("do_checkin()");
1098 return $self->bail_on_events(
1099 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1102 unless( $self->is_renewal ) {
1103 return $self->bail_on_events($self->editor->event)
1104 unless $self->editor->allowed('COPY_CHECKIN');
1107 $self->push_events($self->check_copy_alert());
1108 $self->push_events($self->check_checkin_copy_status());
1111 # the renew code will have already found our circulation object
1112 unless( $self->is_renewal and $self->circ ) {
1114 # first lets see if we have a good old fashioned open circulation
1115 my $circ = $self->editor->search_action_circulation(
1116 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1119 # if not, lets look for other circs we can check in
1120 $circ = $self->editor->search_action_circulation(
1122 target_copy => $self->copy->id,
1123 xact_finish => undef,
1124 stop_fines => [ 'CLAIMSRETURNED', 'LOST', 'LONGOVERDUE' ]
1132 # if the circ is marked as 'claims returned', add the event to the list
1133 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1134 if ($self->circ and $self->circ->stop_fines
1135 and $self->circ->stop_fines eq 'CLAIMSRETURNED');
1137 # handle the overridable events
1138 $self->override_events unless $self->is_renewal;
1139 return if $self->bail_out;
1143 $self->editor->search_action_transit_copy(
1144 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1148 $self->checkin_handle_circ;
1149 return if $self->bail_out;
1150 $self->checkin_changed(1);
1152 } elsif( $self->transit ) {
1153 my $hold_transit = $self->process_received_transit;
1154 $self->checkin_changed(1);
1156 if( $self->bail_out ) {
1157 $self->checkin_flesh_events;
1161 if( my $e = $self->check_checkin_copy_status() ) {
1162 # If the original copy status is special, alert the caller
1163 return $self->bail_on_events($e);
1166 if( $hold_transit ) {
1167 $self->checkin_flesh_events;
1172 if( $self->is_renewal ) {
1173 $self->push_events(OpenILS::Event->new('SUCCESS'));
1177 # ------------------------------------------------------------------------------
1178 # Circulations and transits are now closed where necessary. Now go on to see if
1179 # this copy can fulfill a hold or needs to be routed to a different location
1180 # ------------------------------------------------------------------------------
1182 if( $self->attempt_checkin_hold_capture() ) {
1183 return if $self->bail_out;
1185 } else { # not needed for a hold
1187 my $circ_lib = (ref $self->copy->circ_lib) ?
1188 $self->copy->circ_lib->id : $self->copy->circ_lib;
1190 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1192 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1194 $self->checkin_handle_precat();
1195 return if $self->bail_out;
1199 $self->checkin_build_copy_transit();
1200 return if $self->bail_out;
1201 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1206 $self->reshelve_copy;
1207 return if $self->bail_out;
1209 unless($self->checkin_changed) {
1211 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1212 my $stat = (ref $self->copy->status) ? $self->copy->status->id : $self->copy->status;
1214 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1215 if( $stat == $U->copy_status_from_name('on holds shelf')->id );
1216 $self->bail_out(1); # no need to commit anything
1219 $self->push_events(OpenILS::Event->new('SUCCESS'))
1220 unless @{$self->events};
1223 $self->checkin_flesh_events;
1229 my $copy = $self->copy;
1230 my $force = $self->force;
1232 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1235 $stat != $U->copy_status_from_name('on holds shelf')->id and
1236 $stat != $U->copy_status_from_name('available')->id and
1237 $stat != $U->copy_status_from_name('cataloging')->id and
1238 $stat != $U->copy_status_from_name('in transit')->id and
1239 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1241 $copy->status( $U->copy_status_from_name('reshelving') );
1243 $self->checkin_changed(1);
1248 sub checkin_handle_precat {
1250 my $copy = $self->copy;
1251 my $catstat = $U->copy_status_from_name('cataloging');
1253 if( $self->is_precat and ($copy->status != $catstat->id) ) {
1254 $copy->status($catstat);
1255 $self->update_copy();
1256 $self->checkin_changed(1);
1257 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1262 sub checkin_build_copy_transit {
1264 my $copy = $self->copy;
1265 my $transit = Fieldmapper::action::transit_copy->new;
1267 $transit->source($self->editor->requestor->ws_ou);
1268 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1269 $transit->target_copy($copy->id);
1270 $transit->source_send_time('now');
1271 $transit->copy_status( (ref $copy->status) ? $copy->status->id : $copy->status );
1273 return $self->bail_on_events($self->editor->event)
1274 unless $self->editor->create_action_transit_copy($transit);
1276 $copy->status($U->copy_status_from_name('in transit'));
1278 $self->checkin_changed(1);
1282 sub attempt_checkin_hold_capture {
1284 my $copy = $self->copy;
1286 # See if this copy can fulfill any holds
1287 my ($hold) = $holdcode->find_nearest_permitted_hold(
1288 OpenSRF::AppSession->create('open-ils.storage'),
1289 $copy, $self->editor->requestor );
1292 $logger->debug("circulator: no potential permitted".
1293 "holds found for copy ".$copy->barcode);
1297 $logger->info("circulator: found permitted hold ".
1298 $hold->id . " for copy, capturing...");
1300 $hold->current_copy($copy->id);
1301 $hold->capture_time('now');
1303 # prevent some DB errors
1304 $hold->clear_fulfillment_time;
1305 $hold->clear_fulfillment_staff;
1306 $hold->clear_fulfillment_lib;
1307 $hold->clear_expire_time;
1309 $self->bail_on_events($self->editor->event)
1310 unless $self->editor->update_action_hold_request($hold);
1312 $self->checkin_changed(1);
1314 return 1 if $self->bail_out;
1316 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1318 # This hold was captured in the correct location
1319 $copy->status( $U->copy_status_from_name('on holds shelf') );
1320 $self->push_events(OpenILS::Event->new('SUCCESS'));
1324 # Hold needs to be picked up elsewhere. Build a hold
1325 # transit and route the item.
1326 $self->checkin_build_hold_transit();
1327 $copy->status($U->copy_status_from_name('in transit') );
1328 return 1 if $self->bail_out;
1330 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1333 # make sure we save the copy status
1339 sub checkin_build_hold_transit {
1342 my $copy = $self->copy;
1343 my $hold = $self->hold;
1344 my $trans = Fieldmapper::action::hold_transit_copy->new;
1346 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
1347 $trans->hold($hold->id);
1348 $trans->source($self->editor->requestor->ws_ou);
1349 $trans->dest($hold->pickup_lib);
1350 $trans->source_send_time("now");
1351 $trans->target_copy($copy->id);
1353 # when the copy gets to its destination, it will recover
1354 # this status - put it onto the holds shelf
1355 $trans->copy_status($U->copy_status_from_name('on holds shelf')->id);
1357 return $self->bail_on_events($self->editor->event)
1358 unless $self->editor->create_action_hold_transit_copy($trans);
1363 sub process_received_transit {
1365 my $copy = $self->copy;
1366 my $copyid = $self->copy->id;
1368 my $status_name = $U->copy_status_to_name($copy->status);
1369 $logger->debug("circulator: attempting transit receive on ".
1370 "copy $copyid. Copy status is $status_name");
1372 my $transit = $self->transit;
1374 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1375 $logger->activity("Fowarding transit on copy which is destined ".
1376 "for a different location. copy=$copyid,current ".
1377 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1379 $self->bail_on_events(
1380 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1383 # The transit is received, set the receive time
1384 $transit->dest_recv_time('now');
1385 $self->bail_on_events($self->editor->event)
1386 unless $self->editor->update_action_transit_copy($transit);
1388 my $hold_transit = $self->editor->search_action_hold_transit_copy(
1389 { hold => $transit->id }
1392 $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1393 $copy->status( $transit->copy_status );
1394 $self->update_copy();
1395 return if $self->bail_out;
1397 my $ishold = ($hold_transit) ? 1 : 0;
1400 OpenILS::Event->new(
1403 payload => { transit => $transit, holdtransit => $hold_transit } ));
1405 return $hold_transit;
1409 sub checkin_handle_circ {
1413 my $circ = $self->circ;
1414 my $copy = $self->copy;
1418 # backdate the circ if necessary
1419 if($self->backdate) {
1420 $self->checkin_handle_backdate;
1421 return if $self->bail_out;
1424 if(!$circ->stop_fines) {
1425 $circ->stop_fines('CHECKIN');
1426 $circ->stop_fines('RENEW') if $self->is_renewal;
1427 $circ->stop_fines_time('now');
1430 # see if there are any fines owed on this circ. if not, close it
1431 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1432 $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1434 # Set the checkin vars since we have the item
1435 $circ->checkin_time('now');
1436 $circ->checkin_staff($self->editor->requestor->id);
1437 $circ->checkin_lib($self->editor->requestor->ws_ou);
1439 $self->copy->status($U->copy_status_from_name('reshelving'));
1442 return $self->bail_on_events($self->editor->event)
1443 unless $self->editor->update_action_circulation($circ);
1447 sub checkin_handle_backdate {
1450 my $bills = $self->editor->search_money_billing(
1451 { billing_ts => { ">=" => $self->backdate }, "xact" => $self->circ->id }
1454 for my $bill (@$bills) {
1455 if( !$bill->voided or $bill->voided =~ /f/i ) {
1457 my $n = $bill->note || "";
1458 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1460 $self->bail_on_events($self->editor->event)
1461 unless $self->editor->update_money_billing($bill);
1468 # XXX Legacy version for Circ.pm support
1469 sub _checkin_handle_backdate {
1470 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1472 my $bills = $session->request(
1473 "open-ils.storage.direct.money.billing.search_where.atomic",
1474 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1477 for my $bill (@$bills) {
1479 my $n = $bill->note || "";
1480 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1481 my $s = $session->request(
1482 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1483 return $U->DB_UPDATE_FAILED($bill) unless $s;
1493 sub find_patron_from_copy {
1495 my $circs = $self->editor->search_action_circulation(
1496 { target_copy => $self->copy->id, stop_fines_time => undef });
1497 my $circ = $circs->[0];
1498 return unless $circ;
1499 my $u = $self->editor->retrieve_actor_user($circ->usr)
1500 or return $self->bail_on_events($self->editor->event);
1504 sub check_checkin_copy_status {
1506 my $copy = $self->copy;
1512 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1515 if( $status == $U->copy_status_from_name('available')->id ||
1516 $status == $U->copy_status_from_name('checked out')->id ||
1517 $status == $U->copy_status_from_name('in process')->id ||
1518 $status == $U->copy_status_from_name('in transit')->id ||
1519 $status == $U->copy_status_from_name('reshelving')->id );
1521 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1522 if( $status == $U->copy_status_from_name('lost')->id );
1524 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1525 if( $status == $U->copy_status_from_name('missing')->id );
1527 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1532 # --------------------------------------------------------------------------
1533 # On checkin, we need to return as many relevant objects as we can
1534 # --------------------------------------------------------------------------
1535 sub checkin_flesh_events {
1538 for my $evt (@{$self->events}) {
1541 $payload->{copy} = $U->unflesh_copy($self->copy);
1542 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1543 $payload->{circ} = $self->circ;
1544 $payload->{transit} = $self->transit;
1545 $payload->{hold} = $self->hold;
1547 $evt->{payload} = $payload;
1552 my( $self, $msg ) = @_;
1553 my $bc = ($self->copy) ? $self->copy->barcode :
1556 my $usr = ($self->patron) ? $self->patron->id : "";
1557 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1558 ", recipient=$usr, copy=$bc");
1564 $self->log_me("do_renew()");
1565 $self->is_renewal(1);
1567 unless( $self->is_renewal ) {
1568 return $self->bail_on_events($self->editor->events)
1569 unless $self->editor->allowed('RENEW_CIRC');
1572 # Make sure there is an open circ to renew that is not
1573 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1574 my $circ = $self->editor->search_action_circulation(
1575 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1577 return $self->bail_on_events($self->editor->event) unless $circ;
1579 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1580 if $circ->renewal_remaining < 1;
1582 # -----------------------------------------------------------------
1584 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1585 $self->renewal_remaining(0) if $self->renewal_remaining < 0;
1588 $self->run_renew_permit;
1591 $self->do_checkin();
1592 return if $self->bail_out;
1594 unless( $self->permit_override ) {
1596 return if $self->bail_out;
1597 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1598 $self->remove_event('ITEM_NOT_CATALOGED');
1601 $self->override_events;
1602 return if $self->bail_out;
1605 $self->do_checkout();
1610 my( $self, $evt ) = @_;
1611 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1612 $logger->debug("circulator: removing event from list: $evt");
1613 my @events = @{$self->events};
1614 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1619 my( $self, $evt ) = @_;
1620 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1621 return grep { $_->{textcode} eq $evt } @{$self->events};
1626 sub run_renew_permit {
1628 my $runner = $self->script_runner;
1630 $runner->load($self->circ_permit_renew);
1631 my $result = $runner->run or
1632 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1633 my $events = $result->{events};
1635 $logger->activity("circ_permit_renew for user ".
1636 $self->patron->id." returned events: @$events") if @$events;
1638 $self->push_events(OpenILS::Event->new($_)) for @$events;