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 unless( $self->editor->requestor->id == $self->patron->id ) {
480 return $self->bail_on_events($self->editor->event)
481 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
484 $self->do_copy_checks();
485 return if $self->bail_out;
486 $self->run_patron_permit_scripts();
487 $self->run_copy_permit_scripts()
488 unless $self->is_precat or $self->is_noncat;
489 $self->override_events() unless $self->is_renewal;
490 return if $self->bail_out;
492 if( $self->is_precat ) {
495 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
496 return $self->bail_out(1) unless $self->is_renewal;
502 payload => $self->mk_permit_key));
508 my $copy = $self->copy;
511 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
513 # We cannot check out a copy if it is in-transit
514 if( $stat == $U->copy_status_from_name('in transit')->id ) {
515 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
518 $self->handle_claims_returned();
519 return if $self->bail_out;
521 # no claims returned circ was found, check if there is any open circ
522 unless( $self->is_renewal ) {
523 my $circs = $self->editor->search_action_circulation(
524 { target_copy => $copy->id, stop_fines_time => undef }
527 return $self->bail_on_events(
528 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
533 # ---------------------------------------------------------------------
534 # This pushes any patron-related events into the list but does not
535 # set bail_out for any events
536 # ---------------------------------------------------------------------
537 sub run_patron_permit_scripts {
539 my $runner = $self->script_runner;
540 my $patronid = $self->patron->id;
542 # ---------------------------------------------------------------------
543 # Find all of the fatal penalties currently set on the user
544 # ---------------------------------------------------------------------
545 my $penalties = $U->update_patron_penalties(
546 authtoken => $self->editor->authtoken,
547 patron => $self->patron,
550 $penalties = $penalties->{fatal_penalties};
552 # ---------------------------------------------------------------------
553 # Now run the patron permit script
554 # ---------------------------------------------------------------------
555 $runner->load($self->circ_permit_patron);
556 my $result = $runner->run or
557 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
559 my $patron_events = $result->{events};
561 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
563 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
565 $self->push_events(@allevents);
569 sub run_copy_permit_scripts {
571 my $copy = $self->copy || return;
572 my $runner = $self->script_runner;
574 # ---------------------------------------------------------------------
575 # Capture all of the copy permit events
576 # ---------------------------------------------------------------------
577 $runner->load($self->circ_permit_copy);
578 my $result = $runner->run or
579 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
580 my $copy_events = $result->{events};
582 # ---------------------------------------------------------------------
583 # Now collect all of the events together
584 # ---------------------------------------------------------------------
586 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
588 # See if this copy has an alert message
589 my $ae = $self->check_copy_alert();
590 push( @allevents, $ae ) if $ae;
592 # uniquify the events
593 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
594 @allevents = values %hash;
597 # If the script says the copy is not available, put the status
598 # in as the payload for that event
599 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
601 $_->{payload} = $stat if
602 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
605 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
607 $self->push_events(@allevents);
611 sub check_copy_alert {
613 return OpenILS::Event->new(
614 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
615 if $self->copy and $self->copy->alert_message;
621 # --------------------------------------------------------------------------
622 # If the call is overriding and has permissions to override every collected
623 # event, the are cleared. Any event that the caller does not have
624 # permission to override, will be left in the event list and bail_out will
626 # XXX We need code in here to cancel any holds/transits on copies
627 # that are being force-checked out
628 # --------------------------------------------------------------------------
629 sub override_events {
631 my @events = @{$self->events};
632 return unless @events;
634 if(!$self->override) {
635 return $self->bail_out(1)
636 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
641 for my $e (@events) {
642 my $tc = $e->{textcode};
643 next if $tc eq 'SUCCESS';
644 my $ov = "$tc.override";
645 $logger->info("circulator: attempting to override event: $ov");
647 return $self->bail_on_events($self->editor->event)
648 unless( $self->editor->allowed($ov) );
653 # --------------------------------------------------------------------------
654 # If there is an open claimsreturn circ on the requested copy, close the
655 # circ if overriding, otherwise bail out
656 # --------------------------------------------------------------------------
657 sub handle_claims_returned {
659 my $copy = $self->copy;
661 my $CR = $self->editor->search_action_circulation(
663 target_copy => $copy->id,
664 stop_fines => 'CLAIMSRETURNED',
665 checkin_time => undef,
669 return unless ($CR = $CR->[0]);
673 # - If the caller has set the override flag, we will check the item in
674 if($self->override) {
676 $CR->checkin_time('now');
677 $CR->checkin_lib($self->editor->requestor->ws_ou);
678 $CR->checkin_staff($self->editor->requestor->id);
680 $evt = $self->editor->event
681 unless $self->editor->update_action_circulation($CR);
684 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
687 $self->bail_on_events($evt) if $evt;
692 # --------------------------------------------------------------------------
693 # This performs the checkout
694 # --------------------------------------------------------------------------
698 # make sure perms are good if this isn't a renewal
699 unless( $self->is_renewal ) {
700 return $self->bail_on_events($self->editor->event)
701 unless( $self->editor->allowed('COPY_CHECKOUT') );
704 # verify the permit key
705 unless( $self->check_permit_key ) {
706 if( $self->permit_override ) {
707 return $self->bail_on_events($self->editor->event)
708 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
710 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
714 # if this is a non-cataloged circ, build the circ and finish
715 if( $self->is_noncat ) {
716 $self->checkout_noncat;
718 OpenILS::Event->new('SUCCESS',
719 payload => { noncat_circ => $self->circ }));
723 if( $self->is_precat ) {
724 $self->script_runner->insert("environment.isPrecat", 1, 1);
725 $self->make_precat_copy;
726 return if $self->bail_out;
728 } elsif( $self->copy->call_number == -1 ) {
729 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
732 $self->do_copy_checks;
733 return if $self->bail_out;
735 $self->run_checkout_scripts();
736 return if $self->bail_out;
738 $self->build_checkout_circ_object();
739 return if $self->bail_out;
741 $self->apply_modified_due_date();
742 return if $self->bail_out;
744 return $self->bail_on_events($self->editor->event)
745 unless $self->editor->create_action_circulation($self->circ);
747 $self->copy->status($U->copy_status_from_name('checked out'));
749 return if $self->bail_out;
751 $self->handle_checkout_holds();
752 return if $self->bail_out;
754 # ------------------------------------------------------------------------------
755 # Update the patron penalty info in the DB
756 # ------------------------------------------------------------------------------
757 $U->update_patron_penalties(
758 authtoken => $self->editor->authtoken,
759 patron => $self->patron,
763 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
765 OpenILS::Event->new('SUCCESS',
767 copy => $U->unflesh_copy($self->copy),
770 holds_fulfilled => $self->fulfilled_holds,
778 my $copy = $self->copy;
780 my $stat = $copy->status if ref $copy->status;
781 my $loc = $copy->location if ref $copy->location;
782 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
784 $copy->status($stat->id) if $stat;
785 $copy->location($loc->id) if $loc;
786 $copy->circ_lib($circ_lib->id) if $circ_lib;
788 return $self->bail_on_events($self->editor->event)
789 unless $self->editor->update_asset_copy($self->copy);
791 $copy->status($stat) if $stat;
792 $copy->location($loc) if $loc;
793 $copy->circ_lib($circ_lib) if $circ_lib;
798 my( $self, @evts ) = @_;
799 $self->push_events(@evts);
803 sub handle_checkout_holds {
806 my $copy = $self->copy;
807 my $patron = $self->patron;
808 my $holds = $self->editor->search_action_hold_request(
809 { current_copy => $copy->id , fulfillment_time => undef });
813 # XXX We should only fulfill one hold here...
814 # XXX If a hold was transited to the user who is checking out
815 # the item, we need to make sure that hold is what's grabbed
818 # for now, just sort by id to get what should be the oldest hold
819 $holds = [ sort { $a->id <=> $b->id } @$holds ];
820 my @myholds = grep { $_->usr eq $patron->id } @$holds;
821 my @altholds = grep { $_->usr ne $patron->id } @$holds;
824 my $hold = $myholds[0];
826 $logger->debug("Related hold found in checkout: " . $hold->id );
828 $hold->current_copy($copy->id); # just make sure it's set
829 # if the hold was never officially captured, capture it.
830 $hold->capture_time('now') unless $hold->capture_time;
831 $hold->fulfillment_time('now');
832 return $self->bail_on_events($self->editor->event)
833 unless $self->editor->update_action_hold_request($hold);
835 push( @fulfilled, $hold->id );
838 # If there are any holds placed for other users that point to this copy,
839 # then we need to un-target those holds so the targeter can pick a new copy
842 $logger->info("Un-targeting hold ".$_->id.
843 " because copy ".$copy->id." is getting checked out");
845 $_->clear_current_copy;
846 return $self->bail_on_event($self->editor->event)
847 unless $self->editor->update_action_hold_request($_);
851 $self->fulfilled_holds(\@fulfilled);
856 sub run_checkout_scripts {
860 my $runner = $self->script_runner;
861 $runner->load($self->circ_duration);
863 my $result = $runner->run or
864 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
866 my $duration = $result->{durationRule};
867 my $dur_level = $result->{durationLevel};
868 my $recurring = $result->{recurringFinesRule};
869 my $max_fine = $result->{maxFine};
870 my $rec_fines_level = $result->{recurringFinesLevel};
872 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
873 return $self->bail_on_events($evt) if $evt;
874 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
875 return $self->bail_on_events($evt) if $evt;
876 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
877 return $self->bail_on_events($evt) if $evt;
879 $self->duration_level($dur_level);
880 $self->recurring_fines_level($rec_fines_level);
881 $self->duration_rule($duration);
882 $self->recurring_fines_rule($recurring);
883 $self->max_fine_rule($max_fine);
887 sub build_checkout_circ_object {
890 my $circ = Fieldmapper::action::circulation->new;
891 my $duration = $self->duration_rule;
892 my $max = $self->max_fine_rule;
893 my $recurring = $self->recurring_fines_rule;
894 my $copy = $self->copy;
895 my $patron = $self->patron;
896 my $dur_level = $self->duration_level;
897 my $rec_level = $self->recurring_fines_level;
899 $circ->duration( $duration->shrt ) if ($dur_level == 1);
900 $circ->duration( $duration->normal ) if ($dur_level == 2);
901 $circ->duration( $duration->extended ) if ($dur_level == 3);
903 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
904 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
905 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
907 $circ->duration_rule( $duration->name );
908 $circ->recuring_fine_rule( $recurring->name );
909 $circ->max_fine_rule( $max->name );
910 $circ->max_fine( $max->amount );
912 $circ->fine_interval($recurring->recurance_interval);
913 $circ->renewal_remaining( $duration->max_renewals );
914 $circ->target_copy( $copy->id );
915 $circ->usr( $patron->id );
916 $circ->circ_lib( $self->circ_lib );
918 if( $self->is_renewal ) {
919 $circ->opac_renewal(1);
920 $circ->renewal_remaining($self->renewal_remaining);
921 $circ->circ_staff($self->editor->requestor->id);
925 # if the user provided an overiding checkout time,
926 # (e.g. the checkout really happened several hours ago), then
927 # we apply that here. Does this need a perm??
928 $circ->xact_start(clense_ISO8601($self->checkout_time))
929 if $self->checkout_time;
931 # if a patron is renewing, 'requestor' will be the patron
932 $circ->circ_staff($self->editor->requestor->id);
933 $circ->due_date( $self->create_due_date($circ->duration) );
939 sub apply_modified_due_date {
941 my $circ = $self->circ;
942 my $copy = $self->copy;
944 if( $self->due_date ) {
946 return $self->bail_on_events($self->editor->event)
947 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
949 $circ->due_date(clense_ISO8601($self->due_date));
953 # if the due_date lands on a day when the location is closed
956 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
958 $logger->info("circ searching for closed date overlap on lib $org".
959 " with an item due date of ".$circ->due_date );
961 my $dateinfo = $U->storagereq(
962 'open-ils.storage.actor.org_unit.closed_date.overlap',
963 $org, $circ->due_date );
966 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
967 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
969 # XXX make the behavior more dynamic
970 # for now, we just push the due date to after the close date
971 $circ->due_date($dateinfo->{end});
978 sub create_due_date {
979 my( $self, $duration ) = @_;
980 my ($sec,$min,$hour,$mday,$mon,$year) =
981 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
982 $year += 1900; $mon += 1;
983 my $due_date = sprintf(
984 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
985 $year, $mon, $mday, $hour, $min, $sec);
991 sub make_precat_copy {
993 my $copy = $self->copy;
996 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
998 $copy->editor($self->editor->requestor->id);
999 $copy->edit_date('now');
1000 $copy->dummy_title($self->dummy_title);
1001 $copy->dummy_author($self->dummy_author);
1003 $self->update_copy();
1007 $logger->info("circulator: Creating a new precataloged ".
1008 "copy in checkout with barcode " . $self->copy_barcode);
1010 $copy = Fieldmapper::asset::copy->new;
1011 $copy->circ_lib($self->circ_lib);
1012 $copy->creator($self->editor->requestor->id);
1013 $copy->editor($self->editor->requestor->id);
1014 $copy->barcode($self->copy_barcode);
1015 $copy->call_number(-1); #special CN for precat materials
1016 $copy->loan_duration(&PRECAT_LOAN_DURATION);
1017 $copy->fine_level(&PRECAT_FINE_LEVEL);
1019 $copy->dummy_title($self->dummy_title || "");
1020 $copy->dummy_author($self->dummy_author || "");
1022 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1024 $self->push_events($self->editor->event);
1028 # this is a little bit of a hack, but we need to
1029 # get the copy into the script runner
1030 $self->script_runner->insert("environment.copy", $copy, 1);
1034 sub checkout_noncat {
1040 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1041 my $count = $self->noncat_count || 1;
1042 my $cotime = clense_ISO8601($self->checkout_time) || "";
1044 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1048 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1049 $self->editor->requestor->id,
1057 $self->push_events($evt);
1069 unless( $self->is_renewal ) {
1070 return $self->bail_on_events($self->editor->event)
1071 unless $self->editor->allowed('COPY_CHECKIN');
1074 $self->push_events($self->check_copy_alert());
1075 $self->push_events($self->check_checkin_copy_status());
1078 # the renew code will have already found our circulation object
1079 unless( $self->is_renewal and $self->circ ) {
1081 # first lets see if we have a good old fashioned open circulation
1082 my $circ = $self->editor->search_action_circulation(
1083 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1086 # if not, lets look for other circs we can check in
1087 $circ = $self->editor->search_action_circulation(
1089 target_copy => $self->copy->id,
1090 xact_finish => undef,
1091 stop_fines => [ 'CLAIMSRETURNED', 'LOST', 'LONGOVERDUE' ]
1099 # if the circ is marked as 'claims returned', add the event to the list
1100 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1101 if ($self->circ and $self->circ->stop_fines
1102 and $self->circ->stop_fines eq 'CLAIMSRETURNED');
1104 # handle the overridable events
1105 $self->override_events unless $self->is_renewal;
1109 $self->editor->search_action_transit_copy(
1110 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1114 $self->checkin_handle_circ;
1115 return if $self->bail_out;
1116 $self->checkin_changed(1);
1118 } elsif( $self->transit ) {
1119 my $hold_transit = $self->process_received_transit;
1120 $self->checkin_changed(1);
1122 if( $self->bail_out ) {
1123 $self->checkin_flesh_events;
1127 if( my $e = $self->check_checkin_copy_status() ) {
1128 # If the original copy status is special, alert the caller
1129 return $self->bail_on_events($e);
1132 if( $hold_transit ) {
1133 $self->checkin_flesh_events;
1138 if( $self->is_renewal ) {
1139 $self->push_events(OpenILS::Event->new('SUCCESS'));
1143 # ------------------------------------------------------------------------------
1144 # Circulations and transits are now closed where necessary. Now go on to see if
1145 # this copy can fulfill a hold or needs to be routed to a different location
1146 # ------------------------------------------------------------------------------
1148 if( $self->attempt_checkin_hold_capture() ) {
1149 return if $self->bail_out;
1151 } else { # not needed for a hold
1153 my $circ_lib = (ref $self->copy->circ_lib) ?
1154 $self->copy->circ_lib->id : $self->copy->circ_lib;
1156 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1158 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1160 $self->checkin_handle_precat();
1161 return if $self->bail_out;
1165 $self->checkin_build_copy_transit();
1166 return if $self->bail_out;
1167 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1172 $self->reshelve_copy;
1173 return if $self->bail_out;
1175 unless($self->checkin_changed) {
1177 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1178 my $stat = (ref $self->copy->status) ? $self->copy->status->id : $self->copy->status;
1180 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1181 if( $stat == $U->copy_status_from_name('on holds shelf')->id );
1182 $self->bail_out(1); # no need to commit anything
1185 $self->push_events(OpenILS::Event->new('SUCCESS'))
1186 unless @{$self->events};
1189 $self->checkin_flesh_events;
1195 my $copy = $self->copy;
1196 my $force = $self->force;
1198 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1201 $stat != $U->copy_status_from_name('on holds shelf')->id and
1202 $stat != $U->copy_status_from_name('available')->id and
1203 $stat != $U->copy_status_from_name('cataloging')->id and
1204 $stat != $U->copy_status_from_name('in transit')->id and
1205 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1207 $copy->status( $U->copy_status_from_name('reshelving') );
1209 $self->checkin_changed(1);
1214 sub checkin_handle_precat {
1216 my $copy = $self->copy;
1217 my $catstat = $U->copy_status_from_name('cataloging');
1219 if( $self->is_precat and ($copy->status != $catstat->id) ) {
1220 $copy->status($catstat);
1221 $self->update_copy();
1222 $self->checkin_changed(1);
1223 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1228 sub checkin_build_copy_transit {
1230 my $copy = $self->copy;
1231 my $transit = Fieldmapper::action::transit_copy->new;
1233 $transit->source($self->editor->requestor->ws_ou);
1234 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1235 $transit->target_copy($copy->id);
1236 $transit->source_send_time('now');
1237 $transit->copy_status( (ref $copy->status) ? $copy->status->id : $copy->status );
1239 return $self->bail_on_events($self->editor->event)
1240 unless $self->editor->create_action_transit_copy($transit);
1242 $copy->status($U->copy_status_from_name('in transit'));
1244 $self->checkin_changed(1);
1248 sub attempt_checkin_hold_capture {
1250 my $copy = $self->copy;
1252 # See if this copy can fulfill any holds
1253 my ($hold) = $holdcode->find_nearest_permitted_hold(
1254 OpenSRF::AppSession->create('open-ils.storage'),
1255 $copy, $self->editor->requestor );
1258 $logger->debug("circulator: no potential permitted".
1259 "holds found for copy ".$copy->barcode);
1263 $logger->info("circulator: found permitted hold ".
1264 $hold->id . " for copy, capturing...");
1266 $hold->current_copy($copy->id);
1267 $hold->capture_time('now');
1269 # prevent some DB errors
1270 $hold->clear_fulfillment_time;
1271 $hold->clear_fulfillment_staff;
1272 $hold->clear_fulfillment_lib;
1273 $hold->clear_expire_time;
1275 $self->bail_on_events($self->editor->event)
1276 unless $self->editor->update_action_hold_request($hold);
1278 $self->checkin_changed(1);
1280 return 1 if $self->bail_out;
1282 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1284 # This hold was captured in the correct location
1285 $copy->status( $U->copy_status_from_name('on holds shelf') );
1286 $self->push_events(OpenILS::Event->new('SUCCESS'));
1290 # Hold needs to be picked up elsewhere. Build a hold
1291 # transit and route the item.
1292 $self->checkin_build_hold_transit();
1293 $copy->status($U->copy_status_from_name('in transit') );
1294 return 1 if $self->bail_out;
1296 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1299 # make sure we save the copy status
1305 sub checkin_build_hold_transit {
1308 my $copy = $self->copy;
1309 my $hold = $self->hold;
1310 my $trans = Fieldmapper::action::hold_transit_copy->new;
1312 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
1313 $trans->hold($hold->id);
1314 $trans->source($self->editor->requestor->ws_ou);
1315 $trans->dest($hold->pickup_lib);
1316 $trans->source_send_time("now");
1317 $trans->target_copy($copy->id);
1318 $trans->copy_status($stat);
1320 return $self->bail_on_events($self->editor->event)
1321 unless $self->editor->create_action_hold_transit_copy($trans);
1326 sub process_received_transit {
1328 my $copy = $self->copy;
1329 my $copyid = $self->copy->id;
1331 my $status_name = $U->copy_status_to_name($copy->status);
1332 $logger->debug("circulator: attempting transit receive on ".
1333 "copy $copyid. Copy status is $status_name");
1335 my $transit = $self->transit;
1337 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1338 $logger->activity("Fowarding transit on copy which is destined ".
1339 "for a different location. copy=$copyid,current ".
1340 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1342 $self->bail_on_events(
1343 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1346 # The transit is received, set the receive time
1347 $transit->dest_recv_time('now');
1348 $self->bail_on_events($self->editor->event)
1349 unless $self->editor->update_action_transit_copy($transit);
1351 my $hold_transit = $self->editor->search_action_hold_transit_copy(
1352 { hold => $transit->id }
1355 $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1356 $copy->status( $transit->copy_status );
1357 $self->update_copy();
1358 return if $self->bail_out;
1360 my $ishold = ($hold_transit) ? 1 : 0;
1363 OpenILS::Event->new(
1366 payload => { transit => $transit, holdtransit => $hold_transit } ));
1368 return $hold_transit;
1372 sub checkin_handle_circ {
1376 my $circ = $self->circ;
1377 my $copy = $self->copy;
1381 # backdate the circ if necessary
1382 if($self->backdate) {
1383 $self->handle_backdate;
1384 return if $self->bail_out;
1387 if(!$circ->stop_fines) {
1388 $circ->stop_fines('CHECKIN');
1389 $circ->stop_fines('RENEW') if $self->is_renewal;
1390 $circ->stop_fines_time('now');
1393 # see if there are any fines owed on this circ. if not, close it
1394 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1395 $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1397 # Set the checkin vars since we have the item
1398 $circ->checkin_time('now');
1399 $circ->checkin_staff($self->editor->requestor->id);
1400 $circ->checkin_lib($self->editor->requestor->ws_ou);
1402 $self->copy->status($U->copy_status_from_name('reshelving'));
1405 return $self->bail_on_events($self->editor->event)
1406 unless $self->editor->update_action_circulation($circ);
1410 sub checkin_handle_backdate {
1413 my $bills = $self->editor->search_money_billing(
1414 { billing_ts => { ">=" => $self->backdate }, "xact" => $self->circ->id }
1417 for my $bill (@$bills) {
1418 if( !$bill->voided or $bill->voided =~ /f/i ) {
1420 my $n = $bill->note || "";
1421 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1423 $self->bail_on_events($self->editor->event)
1424 unless $self->editor->update_money_billing($bill);
1431 # XXX Legacy version for Circ.pm support
1432 sub _checkin_handle_backdate {
1433 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1435 my $bills = $session->request(
1436 "open-ils.storage.direct.money.billing.search_where.atomic",
1437 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1440 for my $bill (@$bills) {
1442 my $n = $bill->note || "";
1443 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1444 my $s = $session->request(
1445 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1446 return $U->DB_UPDATE_FAILED($bill) unless $s;
1456 sub find_patron_from_copy {
1458 my $circs = $self->editor->search_action_circulation(
1459 { target_copy => $self->copy->id, stop_fines_time => undef });
1460 my $circ = $circs->[0];
1461 return unless $circ;
1462 my $u = $self->editor->retrieve_actor_user($circ->usr)
1463 or return $self->bail_on_events($self->editor->event);
1467 sub check_checkin_copy_status {
1469 my $copy = $self->copy;
1475 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1478 if( $status == $U->copy_status_from_name('available')->id ||
1479 $status == $U->copy_status_from_name('checked out')->id ||
1480 $status == $U->copy_status_from_name('in process')->id ||
1481 $status == $U->copy_status_from_name('in transit')->id ||
1482 $status == $U->copy_status_from_name('reshelving')->id );
1484 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1485 if( $status == $U->copy_status_from_name('lost')->id );
1487 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1488 if( $status == $U->copy_status_from_name('missing')->id );
1490 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1495 # --------------------------------------------------------------------------
1496 # On checkin, we need to return as many relevant objects as we can
1497 # --------------------------------------------------------------------------
1498 sub checkin_flesh_events {
1501 for my $evt (@{$self->events}) {
1504 $payload->{copy} = $U->unflesh_copy($self->copy);
1505 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1506 $payload->{circ} = $self->circ;
1507 $payload->{transit} = $self->transit;
1508 $payload->{hold} = $self->hold;
1510 $evt->{payload} = $payload;
1517 $self->is_renewal(1);
1519 #$self->find_patron_from_copy unless $self->patron;
1521 unless( $self->is_renewal ) {
1522 return $self->bail_on_events($self->editor->events)
1523 unless $self->editor->allowed('RENEW_CIRC');
1526 # Make sure there is an open circ to renew that is not
1527 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1528 my $circ = $self->editor->search_action_circulation(
1529 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1531 return $self->bail_on_events($self->editor->event) unless $circ;
1533 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1534 if $circ->renewal_remaining < 1;
1536 # -----------------------------------------------------------------
1538 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1539 $self->renewal_remaining(0) if $self->renewal_remaining < 0;
1542 $self->run_renew_permit;
1545 $self->do_checkin();
1546 return if $self->bail_out;
1548 unless( $self->permit_override ) {
1550 return if $self->bail_out;
1551 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1552 $self->remove_event('ITEM_NOT_CATALOGED');
1555 $self->override_events;
1556 return if $self->bail_out;
1559 $self->do_checkout();
1564 my( $self, $evt ) = @_;
1565 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1566 $logger->debug("circulator: removing event from list: $evt");
1567 my @events = @{$self->events};
1568 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1573 my( $self, $evt ) = @_;
1574 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1575 return grep { $_->{textcode} eq $evt } @{$self->events};
1580 sub run_renew_permit {
1582 my $runner = $self->script_runner;
1584 $runner->load($self->circ_permit_renew);
1585 my $result = $runner->run or
1586 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1587 my $events = $result->{events};
1589 $logger->activity("circ_permit_renew for user ".
1590 $self->patron->id." returned events: @$events") if @$events;
1592 $self->push_events(OpenILS::Event->new($_)) for @$events;