1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::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 "circulator: 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.
132 __PACKAGE__->register_method(
133 method => "run_method",
134 api_name => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136 method => "run_method",
137 api_name => "open-ils.circ.checkout.full.override");
142 my( $self, $conn, $auth, $args ) = @_;
143 translate_legacy_args($args);
144 my $api = $self->api_name;
147 OpenILS::Application::Circ::Circulator->new($auth, %$args);
149 return circ_events($circulator) if $circulator->bail_out;
151 # --------------------------------------------------------------------------
152 # Go ahead and load the script runner to make sure we have all
153 # of the objects we need
154 # --------------------------------------------------------------------------
155 $circulator->is_renewal(1) if $api =~ /renew/;
156 $circulator->is_checkin(1) if $api =~ /checkin/;
157 $circulator->mk_script_runner;
158 return circ_events($circulator) if $circulator->bail_out;
160 $circulator->circ_permit_patron($scripts{circ_permit_patron});
161 $circulator->circ_permit_copy($scripts{circ_permit_copy});
162 $circulator->circ_duration($scripts{circ_duration});
163 $circulator->circ_permit_renew($scripts{circ_permit_renew});
165 $circulator->override(1) if $api =~ /override/o;
167 if( $api =~ /checkout\.permit/ ) {
168 $circulator->do_permit();
170 } elsif( $api =~ /checkout.full/ ) {
172 $circulator->do_permit();
173 unless( $circulator->bail_out ) {
174 $circulator->events([]);
175 $circulator->do_checkout();
178 } elsif( $api =~ /checkout/ ) {
179 $circulator->do_checkout();
181 } elsif( $api =~ /checkin/ ) {
182 $circulator->do_checkin();
184 } elsif( $api =~ /renew/ ) {
185 $circulator->is_renewal(1);
186 $circulator->do_renew();
189 if( $circulator->bail_out ) {
192 # make sure no success event accidentally slip in
194 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
197 my @e = @{$circulator->events};
198 push( @ee, $_->{textcode} ) for @e;
199 $logger->info("circulator: bailing out with events: @ee");
201 $circulator->editor->rollback;
204 $circulator->editor->commit;
207 $circulator->script_runner->cleanup;
209 $conn->respond_complete(circ_events($circulator));
211 unless($circulator->bail_out) {
212 $circulator->do_hold_notify($circulator->notify_hold)
213 if $circulator->notify_hold;
214 $circulator->retarget_holds if $circulator->retarget;
220 my @e = @{$circ->events};
221 # if we have multiple events, SUCCESS should not be one of them;
222 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
223 return (@e == 1) ? $e[0] : \@e;
228 sub translate_legacy_args {
231 if( $$args{barcode} ) {
232 $$args{copy_barcode} = $$args{barcode};
233 delete $$args{barcode};
236 if( $$args{copyid} ) {
237 $$args{copy_id} = $$args{copyid};
238 delete $$args{copyid};
241 if( $$args{patronid} ) {
242 $$args{patron_id} = $$args{patronid};
243 delete $$args{patronid};
246 if( $$args{patron} and !ref($$args{patron}) ) {
247 $$args{patron_id} = $$args{patron};
248 delete $$args{patron};
252 if( $$args{noncat} ) {
253 $$args{is_noncat} = $$args{noncat};
254 delete $$args{noncat};
257 if( $$args{precat} ) {
258 $$args{is_precat} = $$args{precat};
259 delete $$args{precat};
266 # --------------------------------------------------------------------------
267 # This package actually manages all of the circulation logic
268 # --------------------------------------------------------------------------
269 package OpenILS::Application::Circ::Circulator;
270 use strict; use warnings;
271 use vars q/$AUTOLOAD/;
273 use OpenILS::Utils::Fieldmapper;
274 use OpenSRF::Utils::Cache;
275 use Digest::MD5 qw(md5_hex);
276 use DateTime::Format::ISO8601;
277 use OpenILS::Utils::PermitHold;
278 use OpenSRF::Utils qw/:datetime/;
279 use OpenSRF::Utils::SettingsClient;
280 use OpenILS::Application::Circ::Holds;
281 use OpenILS::Application::Circ::Transit;
282 use OpenSRF::Utils::Logger qw(:logger);
283 use OpenILS::Utils::CStoreEditor qw/:funcs/;
284 use OpenILS::Application::Circ::ScriptBuilder;
285 use OpenILS::Const qw/:const/;
287 my $U = "OpenILS::Application::AppUtils";
288 my $holdcode = "OpenILS::Application::Circ::Holds";
289 my $transcode = "OpenILS::Application::Circ::Transit";
294 # --------------------------------------------------------------------------
295 # Add a pile of automagic getter/setter methods
296 # --------------------------------------------------------------------------
297 my @AUTOLOAD_FIELDS = qw/
338 recurring_fines_level
351 cancelled_hold_transit
361 my $type = ref($self) or die "$self is not an object";
363 my $name = $AUTOLOAD;
366 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
367 $logger->error("circulator: $type: invalid autoload field: $name");
368 die "$type: invalid autoload field: $name\n"
373 *{"${type}::${name}"} = sub {
376 $s->{$name} = $v if defined $v;
380 return $self->$name($data);
385 my( $class, $auth, %args ) = @_;
386 $class = ref($class) || $class;
387 my $self = bless( {}, $class );
391 new_editor(xact => 1, authtoken => $auth) );
393 unless( $self->editor->checkauth ) {
394 $self->bail_on_events($self->editor->event);
398 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
400 $self->$_($args{$_}) for keys %args;
403 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
405 # if this is a renewal, default to desk_renewal
406 $self->desk_renewal(1) unless
407 $self->opac_renewal or $self->phone_renewal;
413 # --------------------------------------------------------------------------
414 # True if we should discontinue processing
415 # --------------------------------------------------------------------------
417 my( $self, $bool ) = @_;
418 if( defined $bool ) {
419 $logger->info("circulator: BAILING OUT") if $bool;
420 $self->{bail_out} = $bool;
422 return $self->{bail_out};
427 my( $self, @evts ) = @_;
430 $logger->info("circulator: pushing event ".$e->{textcode});
431 push( @{$self->events}, $e ) unless
432 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
438 my $key = md5_hex( time() . rand() . "$$" );
439 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
440 return $self->permit_key($key);
443 sub check_permit_key {
445 my $key = $self->permit_key;
446 return 0 unless $key;
447 my $k = "oils_permit_key_$key";
448 my $one = $self->cache_handle->get_cache($k);
449 $self->cache_handle->delete_cache($k);
450 return ($one) ? 1 : 0;
454 # --------------------------------------------------------------------------
455 # This builds the script runner environment and fetches most of the
457 # --------------------------------------------------------------------------
458 sub mk_script_runner {
464 qw/copy copy_barcode copy_id patron
465 patron_id patron_barcode volume title editor/;
467 # Translate our objects into the ScriptBuilder args hash
468 $$args{$_} = $self->$_() for @fields;
470 $args->{ignore_user_status} = 1 if $self->is_checkin;
471 $$args{fetch_patron_by_circ_copy} = 1;
472 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
474 if( my $pco = $self->pending_checkouts ) {
475 $logger->info("circulator: we were given a pending checkouts number of $pco");
476 $$args{patronItemsOut} = $pco;
479 # This fetches most of the objects we need
480 $self->script_runner(
481 OpenILS::Application::Circ::ScriptBuilder->build($args));
483 # Now we translate the ScriptBuilder objects back into self
484 $self->$_($$args{$_}) for @fields;
486 my @evts = @{$args->{_events}} if $args->{_events};
488 $logger->debug("circulator: script builder returned events: @evts") if @evts;
492 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
493 if(!$self->is_noncat and
495 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
499 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
500 return $self->bail_on_events(@e);
504 $self->is_precat(1) if $self->copy
505 and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
507 # We can't renew if there is no copy
508 return $self->bail_on_events(@evts) if
509 $self->is_renewal and !$self->copy;
511 # Set some circ-specific flags in the script environment
512 my $evt = "environment";
513 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
515 if( $self->is_noncat ) {
516 $self->script_runner->insert("$evt.isNonCat", 1);
517 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
520 if( $self->is_precat ) {
521 $self->script_runner->insert("environment.isPrecat", 1, 1);
524 $self->script_runner->add_path( $_ ) for @$script_libs;
532 # --------------------------------------------------------------------------
533 # Does the circ permit work
534 # --------------------------------------------------------------------------
538 $self->log_me("do_permit()");
540 unless( $self->editor->requestor->id == $self->patron->id ) {
541 return $self->bail_on_events($self->editor->event)
542 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
545 $self->check_captured_holds();
546 $self->do_copy_checks();
547 return if $self->bail_out;
548 $self->run_patron_permit_scripts();
549 $self->run_copy_permit_scripts()
550 unless $self->is_precat or $self->is_noncat;
551 $self->override_events() unless $self->is_renewal;
552 return if $self->bail_out;
554 if( $self->is_precat ) {
557 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
558 return $self->bail_out(1) unless $self->is_renewal;
564 payload => $self->mk_permit_key));
568 sub check_captured_holds {
570 my $copy = $self->copy;
571 my $patron = $self->patron;
573 return undef unless $copy;
575 my $s = $U->copy_status($copy->status)->id;
576 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
577 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
579 # Item is on the holds shelf, make sure it's going to the right person
580 my $holds = $self->editor->search_action_hold_request(
583 current_copy => $copy->id ,
584 capture_time => { '!=' => undef },
585 cancel_time => undef,
586 fulfillment_time => undef
592 if( $holds and $$holds[0] ) {
593 return undef if $$holds[0]->usr == $patron->id;
596 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
598 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
604 my $copy = $self->copy;
607 my $stat = $U->copy_status($copy->status)->id;
609 # We cannot check out a copy if it is in-transit
610 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
611 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
614 $self->handle_claims_returned();
615 return if $self->bail_out;
617 # no claims returned circ was found, check if there is any open circ
618 unless( $self->is_renewal ) {
619 my $circs = $self->editor->search_action_circulation(
620 { target_copy => $copy->id, checkin_time => undef }
623 return $self->bail_on_events(
624 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
629 sub send_penalty_request {
631 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
632 $self->penalty_request(
634 'open-ils.penalty.patron_penalty.calculate',
636 authtoken => $self->editor->authtoken,
637 patron => $self->patron } ) );
640 sub gather_penalty_request {
642 return [] unless $self->penalty_request;
643 my $data = $self->penalty_request->recv;
645 throw $data if UNIVERSAL::isa($data,'Error');
646 $data = $data->content;
647 return $data->{fatal_penalties};
649 $logger->error("circulator: penalty request returned no data");
653 # ---------------------------------------------------------------------
654 # This pushes any patron-related events into the list but does not
655 # set bail_out for any events
656 # ---------------------------------------------------------------------
657 sub run_patron_permit_scripts {
659 my $runner = $self->script_runner;
660 my $patronid = $self->patron->id;
662 $self->send_penalty_request() unless $self->is_renewal;
664 # ---------------------------------------------------------------------
665 # Now run the patron permit script
666 # ---------------------------------------------------------------------
667 $runner->load($self->circ_permit_patron);
668 my $result = $runner->run or
669 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
671 my $patron_events = $result->{events};
675 # ---------------------------------------------------------------------
676 # this is policy directly in the code, not a good idea in general, but
677 # the penalty server doesn't know anything about renewals, so we
678 # have to strip the event out here
679 my $penalties = ($self->is_renewal) ? [] : $self->gather_penalty_request();
680 # ---------------------------------------------------------------------
682 push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
684 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
686 $self->push_events(@allevents);
690 sub run_copy_permit_scripts {
692 my $copy = $self->copy || return;
693 my $runner = $self->script_runner;
695 # ---------------------------------------------------------------------
696 # Capture all of the copy permit events
697 # ---------------------------------------------------------------------
698 $runner->load($self->circ_permit_copy);
699 my $result = $runner->run or
700 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
701 my $copy_events = $result->{events};
703 # ---------------------------------------------------------------------
704 # Now collect all of the events together
705 # ---------------------------------------------------------------------
707 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
709 # See if this copy has an alert message
710 my $ae = $self->check_copy_alert();
711 push( @allevents, $ae ) if $ae;
713 # uniquify the events
714 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
715 @allevents = values %hash;
718 $_->{payload} = $copy if
719 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
722 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
724 $self->push_events(@allevents);
728 sub check_copy_alert {
730 return undef if $self->is_renewal;
731 return OpenILS::Event->new(
732 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
733 if $self->copy and $self->copy->alert_message;
739 # --------------------------------------------------------------------------
740 # If the call is overriding and has permissions to override every collected
741 # event, the are cleared. Any event that the caller does not have
742 # permission to override, will be left in the event list and bail_out will
744 # XXX We need code in here to cancel any holds/transits on copies
745 # that are being force-checked out
746 # --------------------------------------------------------------------------
747 sub override_events {
749 my @events = @{$self->events};
750 return unless @events;
752 if(!$self->override) {
753 return $self->bail_out(1)
754 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
759 for my $e (@events) {
760 my $tc = $e->{textcode};
761 next if $tc eq 'SUCCESS';
762 my $ov = "$tc.override";
763 $logger->info("circulator: attempting to override event: $ov");
765 return $self->bail_on_events($self->editor->event)
766 unless( $self->editor->allowed($ov) );
771 # --------------------------------------------------------------------------
772 # If there is an open claimsreturn circ on the requested copy, close the
773 # circ if overriding, otherwise bail out
774 # --------------------------------------------------------------------------
775 sub handle_claims_returned {
777 my $copy = $self->copy;
779 my $CR = $self->editor->search_action_circulation(
781 target_copy => $copy->id,
782 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
783 checkin_time => undef,
787 return unless ($CR = $CR->[0]);
791 # - If the caller has set the override flag, we will check the item in
792 if($self->override) {
794 $CR->checkin_time('now');
795 $CR->checkin_lib($self->editor->requestor->ws_ou);
796 $CR->checkin_staff($self->editor->requestor->id);
798 $evt = $self->editor->event
799 unless $self->editor->update_action_circulation($CR);
802 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
805 $self->bail_on_events($evt) if $evt;
810 # --------------------------------------------------------------------------
811 # This performs the checkout
812 # --------------------------------------------------------------------------
816 $self->log_me("do_checkout()");
818 # make sure perms are good if this isn't a renewal
819 unless( $self->is_renewal ) {
820 return $self->bail_on_events($self->editor->event)
821 unless( $self->editor->allowed('COPY_CHECKOUT') );
824 # verify the permit key
825 unless( $self->check_permit_key ) {
826 if( $self->permit_override ) {
827 return $self->bail_on_events($self->editor->event)
828 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
830 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
834 # if this is a non-cataloged circ, build the circ and finish
835 if( $self->is_noncat ) {
836 $self->checkout_noncat;
838 OpenILS::Event->new('SUCCESS',
839 payload => { noncat_circ => $self->circ }));
843 if( $self->is_precat ) {
844 $self->script_runner->insert("environment.isPrecat", 1, 1);
845 $self->make_precat_copy;
846 return if $self->bail_out;
848 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
849 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
852 $self->do_copy_checks;
853 return if $self->bail_out;
855 $self->run_checkout_scripts();
856 return if $self->bail_out;
858 $self->build_checkout_circ_object();
859 return if $self->bail_out;
861 $self->apply_modified_due_date();
862 return if $self->bail_out;
864 return $self->bail_on_events($self->editor->event)
865 unless $self->editor->create_action_circulation($self->circ);
867 # refresh the circ to force local time zone for now
868 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
870 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
872 return if $self->bail_out;
874 $self->handle_checkout_holds();
875 return if $self->bail_out;
877 # ------------------------------------------------------------------------------
878 # Update the patron penalty info in the DB. Run it for permit-overrides or
879 # renewals since both of those cases do not require the penalty server to
880 # run during the permit phase of the checkout
881 # ------------------------------------------------------------------------------
882 if( $self->permit_override or $self->is_renewal ) {
883 $U->update_patron_penalties(
884 authtoken => $self->editor->authtoken,
885 patron => $self->patron,
890 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
892 OpenILS::Event->new('SUCCESS',
894 copy => $U->unflesh_copy($self->copy),
897 holds_fulfilled => $self->fulfilled_holds,
905 my $copy = $self->copy;
907 my $stat = $copy->status if ref $copy->status;
908 my $loc = $copy->location if ref $copy->location;
909 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
911 $copy->status($stat->id) if $stat;
912 $copy->location($loc->id) if $loc;
913 $copy->circ_lib($circ_lib->id) if $circ_lib;
914 $copy->editor($self->editor->requestor->id);
915 $copy->edit_date('now');
916 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
918 return $self->bail_on_events($self->editor->event)
919 unless $self->editor->update_asset_copy($self->copy);
921 $copy->status($U->copy_status($copy->status));
922 $copy->location($loc) if $loc;
923 $copy->circ_lib($circ_lib) if $circ_lib;
928 my( $self, @evts ) = @_;
929 $self->push_events(@evts);
933 sub handle_checkout_holds {
936 my $copy = $self->copy;
937 my $patron = $self->patron;
939 my $holds = $self->editor->search_action_hold_request(
941 current_copy => $copy->id ,
942 cancel_time => undef,
943 fulfillment_time => undef
949 # XXX We should only fulfill one hold here...
950 # XXX If a hold was transited to the user who is checking out
951 # the item, we need to make sure that hold is what's grabbed
954 # for now, just sort by id to get what should be the oldest hold
955 $holds = [ sort { $a->id <=> $b->id } @$holds ];
956 my @myholds = grep { $_->usr eq $patron->id } @$holds;
957 my @altholds = grep { $_->usr ne $patron->id } @$holds;
960 my $hold = $myholds[0];
962 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
964 # if the hold was never officially captured, capture it.
965 $hold->capture_time('now') unless $hold->capture_time;
967 # just make sure it's set correctly
968 $hold->current_copy($copy->id);
970 $hold->fulfillment_time('now');
971 $hold->fulfillment_staff($self->editor->requestor->id);
972 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
974 return $self->bail_on_events($self->editor->event)
975 unless $self->editor->update_action_hold_request($hold);
977 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
979 push( @fulfilled, $hold->id );
982 # If there are any holds placed for other users that point to this copy,
983 # then we need to un-target those holds so the targeter can pick a new copy
986 $logger->info("circulator: un-targeting hold ".$_->id.
987 " because copy ".$copy->id." is getting checked out");
989 # - make the targeter process this hold at next run
990 $_->clear_prev_check_time;
992 # - clear out the targetted copy
993 $_->clear_current_copy;
994 $_->clear_capture_time;
996 return $self->bail_on_event($self->editor->event)
997 unless $self->editor->update_action_hold_request($_);
1001 $self->fulfilled_holds(\@fulfilled);
1006 sub run_checkout_scripts {
1010 my $runner = $self->script_runner;
1011 $runner->load($self->circ_duration);
1013 my $result = $runner->run or
1014 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1016 my $duration = $result->{durationRule};
1017 my $recurring = $result->{recurringFinesRule};
1018 my $max_fine = $result->{maxFine};
1020 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1022 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1023 return $self->bail_on_events($evt) if $evt;
1025 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1026 return $self->bail_on_events($evt) if $evt;
1028 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1029 return $self->bail_on_events($evt) if $evt;
1033 # The item circulates with an unlimited duration
1039 $self->duration_rule($duration);
1040 $self->recurring_fines_rule($recurring);
1041 $self->max_fine_rule($max_fine);
1045 sub build_checkout_circ_object {
1048 my $circ = Fieldmapper::action::circulation->new;
1049 my $duration = $self->duration_rule;
1050 my $max = $self->max_fine_rule;
1051 my $recurring = $self->recurring_fines_rule;
1052 my $copy = $self->copy;
1053 my $patron = $self->patron;
1057 my $dname = $duration->name;
1058 my $mname = $max->name;
1059 my $rname = $recurring->name;
1061 $logger->debug("circulator: building circulation ".
1062 "with duration=$dname, maxfine=$mname, recurring=$rname");
1064 $circ->duration( $duration->shrt )
1065 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1066 $circ->duration( $duration->normal )
1067 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1068 $circ->duration( $duration->extended )
1069 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1071 $circ->recuring_fine( $recurring->low )
1072 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1073 $circ->recuring_fine( $recurring->normal )
1074 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1075 $circ->recuring_fine( $recurring->high )
1076 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1078 $circ->duration_rule( $duration->name );
1079 $circ->recuring_fine_rule( $recurring->name );
1080 $circ->max_fine_rule( $max->name );
1081 $circ->max_fine( $max->amount );
1083 $circ->fine_interval($recurring->recurance_interval);
1084 $circ->renewal_remaining( $duration->max_renewals );
1088 $logger->info("circulator: copy found with an unlimited circ duration");
1089 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1090 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1091 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1092 $circ->renewal_remaining(0);
1095 $circ->target_copy( $copy->id );
1096 $circ->usr( $patron->id );
1097 $circ->circ_lib( $self->circ_lib );
1099 if( $self->is_renewal ) {
1100 $circ->opac_renewal('t') if $self->opac_renewal;
1101 $circ->phone_renewal('t') if $self->phone_renewal;
1102 $circ->desk_renewal('t') if $self->desk_renewal;
1103 $circ->renewal_remaining($self->renewal_remaining);
1104 $circ->circ_staff($self->editor->requestor->id);
1108 # if the user provided an overiding checkout time,
1109 # (e.g. the checkout really happened several hours ago), then
1110 # we apply that here. Does this need a perm??
1111 $circ->xact_start(clense_ISO8601($self->checkout_time))
1112 if $self->checkout_time;
1114 # if a patron is renewing, 'requestor' will be the patron
1115 $circ->circ_staff($self->editor->requestor->id);
1116 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1122 sub apply_modified_due_date {
1124 my $circ = $self->circ;
1125 my $copy = $self->copy;
1127 if( $self->due_date ) {
1129 return $self->bail_on_events($self->editor->event)
1130 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1132 $circ->due_date(clense_ISO8601($self->due_date));
1136 # if the due_date lands on a day when the location is closed
1137 return unless $copy and $circ->due_date;
1139 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1141 # due-date overlap should be determined by the location the item
1142 # is checked out from, not the owning or circ lib of the item
1143 my $org = $self->editor->requestor->ws_ou;
1145 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1146 " with an item due date of ".$circ->due_date );
1148 my $dateinfo = $U->storagereq(
1149 'open-ils.storage.actor.org_unit.closed_date.overlap',
1150 $org, $circ->due_date );
1153 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1154 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1156 # XXX make the behavior more dynamic
1157 # for now, we just push the due date to after the close date
1158 $circ->due_date($dateinfo->{end});
1165 sub create_due_date {
1166 my( $self, $duration ) = @_;
1167 my ($sec,$min,$hour,$mday,$mon,$year) =
1168 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1169 $year += 1900; $mon += 1;
1170 my $due_date = sprintf(
1171 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1172 $year, $mon, $mday, $hour, $min, $sec);
1178 sub make_precat_copy {
1180 my $copy = $self->copy;
1183 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1185 $copy->editor($self->editor->requestor->id);
1186 $copy->edit_date('now');
1187 $copy->dummy_title($self->dummy_title);
1188 $copy->dummy_author($self->dummy_author);
1190 $self->update_copy();
1194 $logger->info("circulator: Creating a new precataloged ".
1195 "copy in checkout with barcode " . $self->copy_barcode);
1197 $copy = Fieldmapper::asset::copy->new;
1198 $copy->circ_lib($self->circ_lib);
1199 $copy->creator($self->editor->requestor->id);
1200 $copy->editor($self->editor->requestor->id);
1201 $copy->barcode($self->copy_barcode);
1202 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1203 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1204 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1206 $copy->dummy_title($self->dummy_title || "");
1207 $copy->dummy_author($self->dummy_author || "");
1209 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1211 $self->push_events($self->editor->event);
1215 # this is a little bit of a hack, but we need to
1216 # get the copy into the script runner
1217 $self->script_runner->insert("environment.copy", $copy, 1);
1221 sub checkout_noncat {
1227 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1228 my $count = $self->noncat_count || 1;
1229 my $cotime = clense_ISO8601($self->checkout_time) || "";
1231 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1235 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1236 $self->editor->requestor->id,
1244 $self->push_events($evt);
1255 $self->log_me("do_checkin()");
1258 return $self->bail_on_events(
1259 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1262 if( $self->checkin_check_holds_shelf() ) {
1263 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1264 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1265 $self->checkin_flesh_events;
1269 unless( $self->is_renewal ) {
1270 return $self->bail_on_events($self->editor->event)
1271 unless $self->editor->allowed('COPY_CHECKIN');
1274 $self->push_events($self->check_copy_alert());
1275 $self->push_events($self->check_checkin_copy_status());
1277 # the renew code will have already found our circulation object
1278 unless( $self->is_renewal and $self->circ ) {
1279 my $circs = $self->editor->search_action_circulation(
1280 { target_copy => $self->copy->id, checkin_time => undef });
1281 $self->circ($$circs[0]);
1283 # for now, just warn if there are multiple open circs on a copy
1284 $logger->warn("circulator: we have ".scalar(@$circs).
1285 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1288 # if the circ is marked as 'claims returned', add the event to the list
1289 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1290 if ($self->circ and $self->circ->stop_fines
1291 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1293 # handle the overridable events
1294 $self->override_events unless $self->is_renewal;
1295 return if $self->bail_out;
1299 $self->editor->search_action_transit_copy(
1300 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1304 $self->checkin_handle_circ;
1305 return if $self->bail_out;
1306 $self->checkin_changed(1);
1308 } elsif( $self->transit ) {
1309 my $hold_transit = $self->process_received_transit;
1310 $self->checkin_changed(1);
1312 if( $self->bail_out ) {
1313 $self->checkin_flesh_events;
1317 if( my $e = $self->check_checkin_copy_status() ) {
1318 # If the original copy status is special, alert the caller
1319 my $ev = $self->events;
1320 $self->events([$e]);
1321 $self->override_events;
1322 return if $self->bail_out;
1326 if( $hold_transit or
1327 $U->copy_status($self->copy->status)->id
1328 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1331 if( $hold_transit ) {
1332 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1334 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1339 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1341 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1342 $self->reshelve_copy(1);
1343 $self->cancelled_hold_transit(1);
1344 $self->notify_hold(0); # don't notify for cancelled holds
1345 return if $self->bail_out;
1349 # hold transited to correct location
1350 $self->checkin_flesh_events;
1355 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1357 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1358 " that is in-transit, but there is no transit.. repairing");
1359 $self->reshelve_copy(1);
1360 return if $self->bail_out;
1363 if( $self->is_renewal ) {
1364 $self->push_events(OpenILS::Event->new('SUCCESS'));
1368 # ------------------------------------------------------------------------------
1369 # Circulations and transits are now closed where necessary. Now go on to see if
1370 # this copy can fulfill a hold or needs to be routed to a different location
1371 # ------------------------------------------------------------------------------
1373 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1374 return if $self->bail_out;
1376 } else { # not needed for a hold
1378 my $circ_lib = (ref $self->copy->circ_lib) ?
1379 $self->copy->circ_lib->id : $self->copy->circ_lib;
1381 if( $self->remote_hold ) {
1382 $circ_lib = $self->remote_hold->pickup_lib;
1383 $logger->warn("circulator: Copy ".$self->copy->barcode.
1384 " is on a remote hold's shelf, sending to $circ_lib");
1387 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1389 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1391 $self->checkin_handle_precat();
1392 return if $self->bail_out;
1396 my $bc = $self->copy->barcode;
1397 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1398 $self->checkin_build_copy_transit($circ_lib);
1399 return if $self->bail_out;
1400 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1404 $self->reshelve_copy;
1405 return if $self->bail_out;
1407 unless($self->checkin_changed) {
1409 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1410 my $stat = $U->copy_status($self->copy->status)->id;
1412 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1413 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1414 $self->bail_out(1); # no need to commit anything
1418 $self->push_events(OpenILS::Event->new('SUCCESS'))
1419 unless @{$self->events};
1423 # ------------------------------------------------------------------------------
1424 # Update the patron penalty info in the DB
1425 # ------------------------------------------------------------------------------
1426 $U->update_patron_penalties(
1427 authtoken => $self->editor->authtoken,
1428 patron => $self->patron,
1429 background => 1 ) if $self->is_checkin;
1431 $self->checkin_flesh_events;
1437 my $force = $self->force || shift;
1438 my $copy = $self->copy;
1440 my $stat = $U->copy_status($copy->status)->id;
1443 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1444 $stat != OILS_COPY_STATUS_CATALOGING and
1445 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1446 $stat != OILS_COPY_STATUS_RESHELVING )) {
1448 $copy->status( OILS_COPY_STATUS_RESHELVING );
1450 $self->checkin_changed(1);
1455 # Returns true if the item is at the current location
1456 # because it was transited there for a hold and the
1457 # hold has not been fulfilled
1458 sub checkin_check_holds_shelf {
1460 return 0 unless $self->copy;
1463 $U->copy_status($self->copy->status)->id ==
1464 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1466 # find the hold that put us on the holds shelf
1467 my $holds = $self->editor->search_action_hold_request(
1469 current_copy => $self->copy->id,
1470 capture_time => { '!=' => undef },
1471 fulfillment_time => undef,
1472 cancel_time => undef,
1477 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1478 $self->reshelve_copy(1);
1482 my $hold = $$holds[0];
1484 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1485 $hold->id. "] for copy ".$self->copy->barcode);
1487 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1488 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1492 $logger->info("circulator: hold is not for here..");
1493 $self->remote_hold($hold);
1498 sub checkin_handle_precat {
1500 my $copy = $self->copy;
1502 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1503 $copy->status(OILS_COPY_STATUS_CATALOGING);
1504 $self->update_copy();
1505 $self->checkin_changed(1);
1506 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1511 sub checkin_build_copy_transit {
1514 my $copy = $self->copy;
1515 my $transit = Fieldmapper::action::transit_copy->new;
1517 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1518 $logger->info("circulator: transiting copy to $dest");
1520 $transit->source($self->editor->requestor->ws_ou);
1521 $transit->dest($dest);
1522 $transit->target_copy($copy->id);
1523 $transit->source_send_time('now');
1524 $transit->copy_status( $U->copy_status($copy->status)->id );
1526 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1528 return $self->bail_on_events($self->editor->event)
1529 unless $self->editor->create_action_transit_copy($transit);
1531 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1533 $self->checkin_changed(1);
1537 sub attempt_checkin_hold_capture {
1539 my $copy = $self->copy;
1541 # See if this copy can fulfill any holds
1542 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1543 $self->editor, $copy, $self->editor->requestor );
1546 $logger->debug("circulator: no potential permitted".
1547 "holds found for copy ".$copy->barcode);
1551 $self->retarget($retarget);
1553 $logger->info("circulator: found permitted hold ".
1554 $hold->id . " for copy, capturing...");
1556 $hold->current_copy($copy->id);
1557 $hold->capture_time('now');
1559 # prevent DB errors caused by fetching
1560 # holds from storage, and updating through cstore
1561 $hold->clear_fulfillment_time;
1562 $hold->clear_fulfillment_staff;
1563 $hold->clear_fulfillment_lib;
1564 $hold->clear_expire_time;
1565 $hold->clear_cancel_time;
1566 $hold->clear_prev_check_time unless $hold->prev_check_time;
1568 $self->bail_on_events($self->editor->event)
1569 unless $self->editor->update_action_hold_request($hold);
1571 $self->checkin_changed(1);
1573 return 1 if $self->bail_out;
1575 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1577 # This hold was captured in the correct location
1578 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1579 $self->push_events(OpenILS::Event->new('SUCCESS'));
1581 #$self->do_hold_notify($hold->id);
1582 $self->notify_hold($hold->id);
1586 # Hold needs to be picked up elsewhere. Build a hold
1587 # transit and route the item.
1588 $self->checkin_build_hold_transit();
1589 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1590 return 1 if $self->bail_out;
1592 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1595 # make sure we save the copy status
1600 sub do_hold_notify {
1601 my( $self, $holdid ) = @_;
1603 $logger->info("circulator: running delayed hold notify process");
1605 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1606 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1608 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1609 hold_id => $holdid, requestor => $self->editor->requestor);
1611 $logger->debug("circulator: built hold notifier");
1613 if(!$notifier->event) {
1615 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1617 my $stat = $notifier->send_email_notify;
1618 if( $stat == '1' ) {
1619 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1623 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1626 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1630 sub retarget_holds {
1631 $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1632 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1633 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1634 # no reason to wait for the return value
1638 sub checkin_build_hold_transit {
1641 my $copy = $self->copy;
1642 my $hold = $self->hold;
1643 my $trans = Fieldmapper::action::hold_transit_copy->new;
1645 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1647 $trans->hold($hold->id);
1648 $trans->source($self->editor->requestor->ws_ou);
1649 $trans->dest($hold->pickup_lib);
1650 $trans->source_send_time("now");
1651 $trans->target_copy($copy->id);
1653 # when the copy gets to its destination, it will recover
1654 # this status - put it onto the holds shelf
1655 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1657 return $self->bail_on_events($self->editor->event)
1658 unless $self->editor->create_action_hold_transit_copy($trans);
1663 sub process_received_transit {
1665 my $copy = $self->copy;
1666 my $copyid = $self->copy->id;
1668 my $status_name = $U->copy_status($copy->status)->name;
1669 $logger->debug("circulator: attempting transit receive on ".
1670 "copy $copyid. Copy status is $status_name");
1672 my $transit = $self->transit;
1674 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1675 # - this item is in-transit to a different location
1677 my $tid = $transit->id;
1678 my $loc = $self->editor->requestor->ws_ou;
1679 my $dest = $transit->dest;
1681 $logger->info("circulator: Fowarding transit on copy which is destined ".
1682 "for a different location. transit=$tid, copy=$copyid, current ".
1683 "location=$loc, destination location=$dest");
1685 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1687 # grab the associated hold object if available
1688 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1689 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1691 return $self->bail_on_events($evt);
1694 # The transit is received, set the receive time
1695 $transit->dest_recv_time('now');
1696 $self->bail_on_events($self->editor->event)
1697 unless $self->editor->update_action_transit_copy($transit);
1699 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1701 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1702 $copy->status( $transit->copy_status );
1703 $self->update_copy();
1704 return if $self->bail_out;
1708 #$self->do_hold_notify($hold_transit->hold);
1709 $self->notify_hold($hold_transit->hold);
1714 OpenILS::Event->new(
1717 payload => { transit => $transit, holdtransit => $hold_transit } ));
1719 return $hold_transit;
1723 sub checkin_handle_circ {
1727 my $circ = $self->circ;
1728 my $copy = $self->copy;
1732 # backdate the circ if necessary
1733 if($self->backdate) {
1734 $self->checkin_handle_backdate;
1735 return if $self->bail_out;
1738 if(!$circ->stop_fines) {
1739 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1740 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1741 $circ->stop_fines_time('now') unless $self->backdate;
1742 $circ->stop_fines_time($self->backdate) if $self->backdate;
1745 # see if there are any fines owed on this circ. if not, close it
1746 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1747 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1749 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1751 # Set the checkin vars since we have the item
1752 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1754 $circ->checkin_staff($self->editor->requestor->id);
1755 $circ->checkin_lib($self->editor->requestor->ws_ou);
1757 my $circ_lib = (ref $self->copy->circ_lib) ?
1758 $self->copy->circ_lib->id : $self->copy->circ_lib;
1759 my $stat = $U->copy_status($self->copy->status)->id;
1761 # If the item is lost/missing and it needs to be sent home, don't
1762 # reshelve the copy, leave it lost/missing so the recipient will know
1763 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1764 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1765 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1768 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1772 return $self->bail_on_events($self->editor->event)
1773 unless $self->editor->update_action_circulation($circ);
1777 sub checkin_handle_backdate {
1780 my $bd = $self->backdate;
1782 # ------------------------------------------------------------------
1783 # clean up the backdate for date comparison
1784 # we want any bills created on or after the backdate
1785 # ------------------------------------------------------------------
1786 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1787 #$bd = "${bd}T23:59:59";
1789 my $bills = $self->editor->search_money_billing(
1791 billing_ts => { '>=' => $bd },
1792 xact => $self->circ->id,
1793 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1797 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1799 for my $bill (@$bills) {
1800 unless( $U->is_true($bill->voided) ) {
1801 $logger->info("backdate voiding bill ".$bill->id);
1803 $bill->void_time('now');
1804 $bill->voider($self->editor->requestor->id);
1805 my $n = $bill->note || "";
1806 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1808 $self->bail_on_events($self->editor->event)
1809 unless $self->editor->update_money_billing($bill);
1817 # XXX Legacy version for Circ.pm support
1818 sub _checkin_handle_backdate {
1819 my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1822 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1823 $bd = "${bd}T23:59:59";
1825 my $bills = $session->request(
1826 "open-ils.storage.direct.money.billing.search_where.atomic",
1827 billing_ts => { '>=' => $bd },
1829 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1832 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1835 for my $bill (@$bills) {
1836 unless( $U->is_true($bill->voided) ) {
1837 $logger->debug("voiding bill ".$bill->id);
1839 $bill->void_time('now');
1840 $bill->voider($requestor->id);
1841 my $n = $bill->note || "";
1842 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1843 my $s = $session->request(
1844 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1845 return $U->DB_UPDATE_FAILED($bill) unless $s;
1859 sub find_patron_from_copy {
1861 my $circs = $self->editor->search_action_circulation(
1862 { target_copy => $self->copy->id, checkin_time => undef });
1863 my $circ = $circs->[0];
1864 return unless $circ;
1865 my $u = $self->editor->retrieve_actor_user($circ->usr)
1866 or return $self->bail_on_events($self->editor->event);
1870 sub check_checkin_copy_status {
1872 my $copy = $self->copy;
1878 my $status = $U->copy_status($copy->status)->id;
1881 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1882 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1883 $status == OILS_COPY_STATUS_IN_PROCESS ||
1884 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1885 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1886 $status == OILS_COPY_STATUS_CATALOGING ||
1887 $status == OILS_COPY_STATUS_RESHELVING );
1889 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1890 if( $status == OILS_COPY_STATUS_LOST );
1892 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1893 if( $status == OILS_COPY_STATUS_MISSING );
1895 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1900 # --------------------------------------------------------------------------
1901 # On checkin, we need to return as many relevant objects as we can
1902 # --------------------------------------------------------------------------
1903 sub checkin_flesh_events {
1906 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1907 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1908 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1912 for my $evt (@{$self->events}) {
1915 $payload->{copy} = $U->unflesh_copy($self->copy);
1916 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1917 $payload->{circ} = $self->circ;
1918 $payload->{transit} = $self->transit;
1919 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1921 # $self->hold may or may not have been replaced with a
1922 # valid hold after processing a cancelled hold
1923 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1925 $evt->{payload} = $payload;
1930 my( $self, $msg ) = @_;
1931 my $bc = ($self->copy) ? $self->copy->barcode :
1934 my $usr = ($self->patron) ? $self->patron->id : "";
1935 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1936 ", recipient=$usr, copy=$bc");
1942 $self->log_me("do_renew()");
1943 $self->is_renewal(1);
1945 # Make sure there is an open circ to renew that is not
1946 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1947 my $circ = $self->editor->search_action_circulation(
1948 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1951 $circ = $self->editor->search_action_circulation(
1953 target_copy => $self->copy->id,
1954 stop_fines => OILS_STOP_FINES_MAX_FINES,
1955 checkin_time => undef
1960 return $self->bail_on_events($self->editor->event) unless $circ;
1962 # A user is not allowed to renew another user's items without permission
1963 unless( $circ->usr eq $self->editor->requestor->id ) {
1964 return $self->bail_on_events($self->editor->events)
1965 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
1968 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1969 if $circ->renewal_remaining < 1;
1971 # -----------------------------------------------------------------
1973 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1976 $self->run_renew_permit;
1979 $self->do_checkin();
1980 return if $self->bail_out;
1982 unless( $self->permit_override ) {
1984 return if $self->bail_out;
1985 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1986 $self->remove_event('ITEM_NOT_CATALOGED');
1989 $self->override_events;
1990 return if $self->bail_out;
1993 $self->do_checkout();
1998 my( $self, $evt ) = @_;
1999 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2000 $logger->debug("circulator: removing event from list: $evt");
2001 my @events = @{$self->events};
2002 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2007 my( $self, $evt ) = @_;
2008 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2009 return grep { $_->{textcode} eq $evt } @{$self->events};
2014 sub run_renew_permit {
2016 my $runner = $self->script_runner;
2018 $runner->load($self->circ_permit_renew);
2019 my $result = $runner->run or
2020 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2021 my $events = $result->{events};
2023 $logger->activity("ciculator: circ_permit_renew for user ".
2024 $self->patron->id." returned events: @$events") if @$events;
2026 $self->push_events(OpenILS::Event->new($_)) for @$events;
2028 $logger->debug("circulator: re-creating script runner to be safe");
2029 $self->mk_script_runner;