1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenSRF::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
15 my $conf = OpenSRF::Utils::SettingsClient->new;
16 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
17 my @pfx = ( @pfx2, "scripts" );
19 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
20 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
21 my $d = $conf->config_value( @pfx, 'circ_duration' );
22 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
23 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
24 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
25 my $lb = $conf->config_value( @pfx2, 'script_path' );
27 $logger->error( "Missing circ script(s)" )
28 unless( $p and $c and $d and $f and $m and $pr );
30 $scripts{circ_permit_patron} = $p;
31 $scripts{circ_permit_copy} = $c;
32 $scripts{circ_duration} = $d;
33 $scripts{circ_recurring_fines}= $f;
34 $scripts{circ_max_fines} = $m;
35 $scripts{circ_permit_renew} = $pr;
37 $lb = [ $lb ] unless ref($lb);
41 "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} ]);
195 my @e = @{$circulator->events};
196 push( @ee, $_->{textcode} ) for @e;
197 $logger->info("circulator: bailing out with events: @ee");
198 $circulator->editor->rollback;
201 $circulator->editor->commit;
204 $circulator->script_runner->cleanup;
206 $conn->respond_complete(circ_events($circulator));
208 unless($circulator->bail_out) {
209 $logger->info("circulator: running delayed hold notify process");
210 $circulator->do_hold_notify($circulator->notify_hold)
211 if $circulator->notify_hold;
217 my @e = @{$circ->events};
218 return (@e == 1) ? $e[0] : \@e;
223 sub translate_legacy_args {
226 if( $$args{barcode} ) {
227 $$args{copy_barcode} = $$args{barcode};
228 delete $$args{barcode};
231 if( $$args{copyid} ) {
232 $$args{copy_id} = $$args{copyid};
233 delete $$args{copyid};
236 if( $$args{patronid} ) {
237 $$args{patron_id} = $$args{patronid};
238 delete $$args{patronid};
241 if( $$args{patron} and !ref($$args{patron}) ) {
242 $$args{patron_id} = $$args{patron};
243 delete $$args{patron};
247 if( $$args{noncat} ) {
248 $$args{is_noncat} = $$args{noncat};
249 delete $$args{noncat};
252 if( $$args{precat} ) {
253 $$args{is_precat} = $$args{precat};
254 delete $$args{precat};
260 # --------------------------------------------------------------------------
261 # This package actually manages all of the circulation logic
262 # --------------------------------------------------------------------------
263 package OpenILS::Application::Circ::Circulator;
264 use strict; use warnings;
265 use vars q/$AUTOLOAD/;
267 use OpenILS::Utils::Fieldmapper;
268 use OpenSRF::Utils::Cache;
269 use Digest::MD5 qw(md5_hex);
270 use DateTime::Format::ISO8601;
271 use OpenILS::Utils::PermitHold;
272 use OpenSRF::Utils qw/:datetime/;
273 use OpenSRF::Utils::SettingsClient;
274 use OpenILS::Application::Circ::Holds;
275 use OpenILS::Application::Circ::Transit;
276 use OpenSRF::Utils::Logger qw(:logger);
277 use OpenILS::Utils::CStoreEditor qw/:funcs/;
278 use OpenILS::Application::Circ::ScriptBuilder;
279 use OpenILS::Const qw/:const/;
281 my $U = "OpenILS::Application::AppUtils";
282 my $holdcode = "OpenILS::Application::Circ::Holds";
283 my $transcode = "OpenILS::Application::Circ::Transit";
288 # --------------------------------------------------------------------------
289 # Add a pile of automagic getter/setter methods
290 # --------------------------------------------------------------------------
291 my @AUTOLOAD_FIELDS = qw/
332 recurring_fines_level
350 my $type = ref($self) or die "$self is not an object";
352 my $name = $AUTOLOAD;
355 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
356 $logger->error("circulator: $type: invalid autoload field: $name");
357 die "$type: invalid autoload field: $name\n"
362 *{"${type}::${name}"} = sub {
365 $s->{$name} = $v if defined $v;
369 return $self->$name($data);
374 my( $class, $auth, %args ) = @_;
375 $class = ref($class) || $class;
376 my $self = bless( {}, $class );
380 new_editor(xact => 1, authtoken => $auth) );
382 unless( $self->editor->checkauth ) {
383 $self->bail_on_events($self->editor->event);
387 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
389 $self->$_($args{$_}) for keys %args;
392 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
398 # --------------------------------------------------------------------------
399 # True if we should discontinue processing
400 # --------------------------------------------------------------------------
402 my( $self, $bool ) = @_;
403 if( defined $bool ) {
404 $logger->info("circulator: BAILING OUT") if $bool;
405 $self->{bail_out} = $bool;
407 return $self->{bail_out};
412 my( $self, @evts ) = @_;
415 $logger->info("circulator: pushing event ".$e->{textcode});
416 push( @{$self->events}, $e ) unless
417 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
423 my $key = md5_hex( time() . rand() . "$$" );
424 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
425 return $self->permit_key($key);
428 sub check_permit_key {
430 my $key = $self->permit_key;
431 return 0 unless $key;
432 my $k = "oils_permit_key_$key";
433 my $one = $self->cache_handle->get_cache($k);
434 $self->cache_handle->delete_cache($k);
435 return ($one) ? 1 : 0;
439 # --------------------------------------------------------------------------
440 # This builds the script runner environment and fetches most of the
442 # --------------------------------------------------------------------------
443 sub mk_script_runner {
449 qw/copy copy_barcode copy_id patron
450 patron_id patron_barcode volume title editor/;
452 # Translate our objects into the ScriptBuilder args hash
453 $$args{$_} = $self->$_() for @fields;
455 $args->{ignore_user_status} = 1 if $self->is_checkin;
456 $$args{fetch_patron_by_circ_copy} = 1;
457 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
459 if( my $pco = $self->pending_checkouts ) {
460 $logger->info("circulator: we were given a pending checkouts number of $pco");
461 $$args{patronItemsOut} = $pco;
464 # This fetches most of the objects we need
465 $self->script_runner(
466 OpenILS::Application::Circ::ScriptBuilder->build($args));
468 # Now we translate the ScriptBuilder objects back into self
469 $self->$_($$args{$_}) for @fields;
471 my @evts = @{$args->{_events}} if $args->{_events};
473 $logger->debug("circulator: script builder returned events: @evts") if @evts;
477 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
478 if(!$self->is_noncat and
480 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
484 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
485 return $self->bail_on_events(@e);
489 $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
491 # We can't renew if there is no copy
492 return $self->bail_on_events(@evts) if
493 $self->is_renewal and !$self->copy;
495 # Set some circ-specific flags in the script environment
496 my $evt = "environment";
497 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
499 if( $self->is_noncat ) {
500 $self->script_runner->insert("$evt.isNonCat", 1);
501 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
504 if( $self->is_precat ) {
505 $self->script_runner->insert("environment.isPrecat", 1, 1);
508 $self->script_runner->add_path( $_ ) for @$script_libs;
516 # --------------------------------------------------------------------------
517 # Does the circ permit work
518 # --------------------------------------------------------------------------
522 $self->log_me("do_permit()");
524 unless( $self->editor->requestor->id == $self->patron->id ) {
525 return $self->bail_on_events($self->editor->event)
526 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
529 $self->check_captured_holds();
530 $self->do_copy_checks();
531 return if $self->bail_out;
532 $self->run_patron_permit_scripts();
533 $self->run_copy_permit_scripts()
534 unless $self->is_precat or $self->is_noncat;
535 $self->override_events() unless $self->is_renewal;
536 return if $self->bail_out;
538 if( $self->is_precat ) {
541 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
542 return $self->bail_out(1) unless $self->is_renewal;
548 payload => $self->mk_permit_key));
552 sub check_captured_holds {
554 my $copy = $self->copy;
555 my $patron = $self->patron;
557 return undef unless $copy;
559 my $s = $U->copy_status($copy->status)->id;
560 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
561 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
563 # Item is on the holds shelf, make sure it's going to the right person
564 my $holds = $self->editor->search_action_hold_request(
567 current_copy => $copy->id ,
568 capture_time => { '!=' => undef },
569 cancel_time => undef,
570 fulfillment_time => undef
576 if( $holds and $$holds[0] ) {
577 return undef if $$holds[0]->usr == $patron->id;
580 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
582 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
588 my $copy = $self->copy;
591 my $stat = $U->copy_status($copy->status)->id;
593 # We cannot check out a copy if it is in-transit
594 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
595 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
598 $self->handle_claims_returned();
599 return if $self->bail_out;
601 # no claims returned circ was found, check if there is any open circ
602 unless( $self->is_renewal ) {
603 my $circs = $self->editor->search_action_circulation(
604 { target_copy => $copy->id, checkin_time => undef }
607 return $self->bail_on_events(
608 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
613 sub send_penalty_request {
615 my $ses = OpenSRF::AppSession->create('open-ils.penalty');
616 $self->penalty_request(
618 'open-ils.penalty.patron_penalty.calculate',
620 authtoken => $self->editor->authtoken,
621 patron => $self->patron } ) );
624 sub gather_penalty_request {
626 return [] unless $self->penalty_request;
627 my $data = $self->penalty_request->recv;
629 $data = $data->content;
630 return $data->{fatal_penalties};
632 $logger->error("circulator: penalty request returned no data");
636 # ---------------------------------------------------------------------
637 # This pushes any patron-related events into the list but does not
638 # set bail_out for any events
639 # ---------------------------------------------------------------------
640 sub run_patron_permit_scripts {
642 my $runner = $self->script_runner;
643 my $patronid = $self->patron->id;
645 $self->send_penalty_request();
647 # ---------------------------------------------------------------------
648 # Now run the patron permit script
649 # ---------------------------------------------------------------------
650 $runner->load($self->circ_permit_patron);
651 my $result = $runner->run or
652 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
654 my $patron_events = $result->{events};
657 my $penalties = $self->gather_penalty_request();
659 for my $p (@$penalties, @$patron_events) {
661 # this is policy directly in the code, not a good idea in general, but
662 # the penalty server doesn't know anything about renewals, so we
663 # have to strip the event out here
664 next if $self->is_renewal and $p eq 'PATRON_EXCEEDS_OVERDUE_COUNT';
667 push( @allevents, OpenILS::Event->new($p))
670 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
672 $self->push_events(@allevents);
676 sub run_copy_permit_scripts {
678 my $copy = $self->copy || return;
679 my $runner = $self->script_runner;
681 # ---------------------------------------------------------------------
682 # Capture all of the copy permit events
683 # ---------------------------------------------------------------------
684 $runner->load($self->circ_permit_copy);
685 my $result = $runner->run or
686 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
687 my $copy_events = $result->{events};
689 # ---------------------------------------------------------------------
690 # Now collect all of the events together
691 # ---------------------------------------------------------------------
693 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
695 # See if this copy has an alert message
696 my $ae = $self->check_copy_alert();
697 push( @allevents, $ae ) if $ae;
699 # uniquify the events
700 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
701 @allevents = values %hash;
704 $_->{payload} = $copy if
705 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
708 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
710 $self->push_events(@allevents);
714 sub check_copy_alert {
716 return undef if $self->is_renewal;
717 return OpenILS::Event->new(
718 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
719 if $self->copy and $self->copy->alert_message;
725 # --------------------------------------------------------------------------
726 # If the call is overriding and has permissions to override every collected
727 # event, the are cleared. Any event that the caller does not have
728 # permission to override, will be left in the event list and bail_out will
730 # XXX We need code in here to cancel any holds/transits on copies
731 # that are being force-checked out
732 # --------------------------------------------------------------------------
733 sub override_events {
735 my @events = @{$self->events};
736 return unless @events;
738 if(!$self->override) {
739 return $self->bail_out(1)
740 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
745 for my $e (@events) {
746 my $tc = $e->{textcode};
747 next if $tc eq 'SUCCESS';
748 my $ov = "$tc.override";
749 $logger->info("circulator: attempting to override event: $ov");
751 return $self->bail_on_events($self->editor->event)
752 unless( $self->editor->allowed($ov) );
757 # --------------------------------------------------------------------------
758 # If there is an open claimsreturn circ on the requested copy, close the
759 # circ if overriding, otherwise bail out
760 # --------------------------------------------------------------------------
761 sub handle_claims_returned {
763 my $copy = $self->copy;
765 my $CR = $self->editor->search_action_circulation(
767 target_copy => $copy->id,
768 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
769 checkin_time => undef,
773 return unless ($CR = $CR->[0]);
777 # - If the caller has set the override flag, we will check the item in
778 if($self->override) {
780 $CR->checkin_time('now');
781 $CR->checkin_lib($self->editor->requestor->ws_ou);
782 $CR->checkin_staff($self->editor->requestor->id);
784 $evt = $self->editor->event
785 unless $self->editor->update_action_circulation($CR);
788 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
791 $self->bail_on_events($evt) if $evt;
796 # --------------------------------------------------------------------------
797 # This performs the checkout
798 # --------------------------------------------------------------------------
802 $self->log_me("do_checkout()");
804 # make sure perms are good if this isn't a renewal
805 unless( $self->is_renewal ) {
806 return $self->bail_on_events($self->editor->event)
807 unless( $self->editor->allowed('COPY_CHECKOUT') );
810 # verify the permit key
811 unless( $self->check_permit_key ) {
812 if( $self->permit_override ) {
813 return $self->bail_on_events($self->editor->event)
814 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
816 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
820 # if this is a non-cataloged circ, build the circ and finish
821 if( $self->is_noncat ) {
822 $self->checkout_noncat;
824 OpenILS::Event->new('SUCCESS',
825 payload => { noncat_circ => $self->circ }));
829 if( $self->is_precat ) {
830 $self->script_runner->insert("environment.isPrecat", 1, 1);
831 $self->make_precat_copy;
832 return if $self->bail_out;
834 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
835 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
838 $self->do_copy_checks;
839 return if $self->bail_out;
841 $self->run_checkout_scripts();
842 return if $self->bail_out;
844 $self->build_checkout_circ_object();
845 return if $self->bail_out;
847 $self->apply_modified_due_date();
848 return if $self->bail_out;
850 return $self->bail_on_events($self->editor->event)
851 unless $self->editor->create_action_circulation($self->circ);
853 # refresh the circ to force local time zone for now
854 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
856 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
858 return if $self->bail_out;
860 $self->handle_checkout_holds();
861 return if $self->bail_out;
864 # ------------------------------------------------------------------------------
865 # Update the patron penalty info in the DB
866 # ------------------------------------------------------------------------------
867 if( $self->permit_override ) {
868 $U->update_patron_penalties(
869 authtoken => $self->editor->authtoken,
870 patron => $self->patron,
875 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
877 OpenILS::Event->new('SUCCESS',
879 copy => $U->unflesh_copy($self->copy),
882 holds_fulfilled => $self->fulfilled_holds,
890 my $copy = $self->copy;
892 my $stat = $copy->status if ref $copy->status;
893 my $loc = $copy->location if ref $copy->location;
894 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
896 $copy->status($stat->id) if $stat;
897 $copy->location($loc->id) if $loc;
898 $copy->circ_lib($circ_lib->id) if $circ_lib;
899 $copy->editor($self->editor->requestor->id);
900 $copy->edit_date('now');
901 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
903 return $self->bail_on_events($self->editor->event)
904 unless $self->editor->update_asset_copy($self->copy);
906 $copy->status($U->copy_status($copy->status));
907 $copy->location($loc) if $loc;
908 $copy->circ_lib($circ_lib) if $circ_lib;
913 my( $self, @evts ) = @_;
914 $self->push_events(@evts);
918 sub handle_checkout_holds {
921 my $copy = $self->copy;
922 my $patron = $self->patron;
924 my $holds = $self->editor->search_action_hold_request(
926 current_copy => $copy->id ,
927 cancel_time => undef,
928 fulfillment_time => undef
934 # XXX We should only fulfill one hold here...
935 # XXX If a hold was transited to the user who is checking out
936 # the item, we need to make sure that hold is what's grabbed
939 # for now, just sort by id to get what should be the oldest hold
940 $holds = [ sort { $a->id <=> $b->id } @$holds ];
941 my @myholds = grep { $_->usr eq $patron->id } @$holds;
942 my @altholds = grep { $_->usr ne $patron->id } @$holds;
945 my $hold = $myholds[0];
947 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
949 # if the hold was never officially captured, capture it.
950 $hold->capture_time('now') unless $hold->capture_time;
952 # just make sure it's set correctly
953 $hold->current_copy($copy->id);
955 $hold->fulfillment_time('now');
956 $hold->fulfillment_staff($self->editor->requestor->id);
957 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
959 return $self->bail_on_events($self->editor->event)
960 unless $self->editor->update_action_hold_request($hold);
962 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
964 push( @fulfilled, $hold->id );
967 # If there are any holds placed for other users that point to this copy,
968 # then we need to un-target those holds so the targeter can pick a new copy
971 $logger->info("circulator: un-targeting hold ".$_->id.
972 " because copy ".$copy->id." is getting checked out");
974 # - make the targeter process this hold at next run
975 $_->clear_prev_check_time;
977 # - clear out the targetted copy
978 $_->clear_current_copy;
979 $_->clear_capture_time;
981 return $self->bail_on_event($self->editor->event)
982 unless $self->editor->update_action_hold_request($_);
986 $self->fulfilled_holds(\@fulfilled);
991 sub run_checkout_scripts {
995 my $runner = $self->script_runner;
996 $runner->load($self->circ_duration);
998 my $result = $runner->run or
999 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1001 my $duration = $result->{durationRule};
1002 my $recurring = $result->{recurringFinesRule};
1003 my $max_fine = $result->{maxFine};
1005 if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1007 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1008 return $self->bail_on_events($evt) if $evt;
1010 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1011 return $self->bail_on_events($evt) if $evt;
1013 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1014 return $self->bail_on_events($evt) if $evt;
1018 # The item circulates with an unlimited duration
1024 $self->duration_rule($duration);
1025 $self->recurring_fines_rule($recurring);
1026 $self->max_fine_rule($max_fine);
1030 sub build_checkout_circ_object {
1033 my $circ = Fieldmapper::action::circulation->new;
1034 my $duration = $self->duration_rule;
1035 my $max = $self->max_fine_rule;
1036 my $recurring = $self->recurring_fines_rule;
1037 my $copy = $self->copy;
1038 my $patron = $self->patron;
1042 my $dname = $duration->name;
1043 my $mname = $max->name;
1044 my $rname = $recurring->name;
1046 $logger->debug("circulator: building circulation ".
1047 "with duration=$dname, maxfine=$mname, recurring=$rname");
1049 $circ->duration( $duration->shrt )
1050 if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1051 $circ->duration( $duration->normal )
1052 if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1053 $circ->duration( $duration->extended )
1054 if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1056 $circ->recuring_fine( $recurring->low )
1057 if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1058 $circ->recuring_fine( $recurring->normal )
1059 if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1060 $circ->recuring_fine( $recurring->high )
1061 if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1063 $circ->duration_rule( $duration->name );
1064 $circ->recuring_fine_rule( $recurring->name );
1065 $circ->max_fine_rule( $max->name );
1066 $circ->max_fine( $max->amount );
1068 $circ->fine_interval($recurring->recurance_interval);
1069 $circ->renewal_remaining( $duration->max_renewals );
1073 $logger->info("circulator: copy found with an unlimited circ duration");
1074 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1075 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1076 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1077 $circ->renewal_remaining(0);
1080 $circ->target_copy( $copy->id );
1081 $circ->usr( $patron->id );
1082 $circ->circ_lib( $self->circ_lib );
1084 if( $self->is_renewal ) {
1085 $circ->opac_renewal(1);
1086 $circ->renewal_remaining($self->renewal_remaining);
1087 $circ->circ_staff($self->editor->requestor->id);
1090 # if the user provided an overiding checkout time,
1091 # (e.g. the checkout really happened several hours ago), then
1092 # we apply that here. Does this need a perm??
1093 $circ->xact_start(clense_ISO8601($self->checkout_time))
1094 if $self->checkout_time;
1096 # if a patron is renewing, 'requestor' will be the patron
1097 $circ->circ_staff($self->editor->requestor->id);
1098 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1104 sub apply_modified_due_date {
1106 my $circ = $self->circ;
1107 my $copy = $self->copy;
1109 if( $self->due_date ) {
1111 return $self->bail_on_events($self->editor->event)
1112 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1114 $circ->due_date(clense_ISO8601($self->due_date));
1118 # if the due_date lands on a day when the location is closed
1119 return unless $copy and $circ->due_date;
1121 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1123 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1124 " with an item due date of ".$circ->due_date );
1126 my $dateinfo = $U->storagereq(
1127 'open-ils.storage.actor.org_unit.closed_date.overlap',
1128 $org, $circ->due_date );
1131 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1132 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1134 # XXX make the behavior more dynamic
1135 # for now, we just push the due date to after the close date
1136 $circ->due_date($dateinfo->{end});
1143 sub create_due_date {
1144 my( $self, $duration ) = @_;
1145 my ($sec,$min,$hour,$mday,$mon,$year) =
1146 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1147 $year += 1900; $mon += 1;
1148 my $due_date = sprintf(
1149 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1150 $year, $mon, $mday, $hour, $min, $sec);
1156 sub make_precat_copy {
1158 my $copy = $self->copy;
1161 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1163 $copy->editor($self->editor->requestor->id);
1164 $copy->edit_date('now');
1165 $copy->dummy_title($self->dummy_title);
1166 $copy->dummy_author($self->dummy_author);
1168 $self->update_copy();
1172 $logger->info("circulator: Creating a new precataloged ".
1173 "copy in checkout with barcode " . $self->copy_barcode);
1175 $copy = Fieldmapper::asset::copy->new;
1176 $copy->circ_lib($self->circ_lib);
1177 $copy->creator($self->editor->requestor->id);
1178 $copy->editor($self->editor->requestor->id);
1179 $copy->barcode($self->copy_barcode);
1180 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1181 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1182 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1184 $copy->dummy_title($self->dummy_title || "");
1185 $copy->dummy_author($self->dummy_author || "");
1187 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1189 $self->push_events($self->editor->event);
1193 # this is a little bit of a hack, but we need to
1194 # get the copy into the script runner
1195 $self->script_runner->insert("environment.copy", $copy, 1);
1199 sub checkout_noncat {
1205 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1206 my $count = $self->noncat_count || 1;
1207 my $cotime = clense_ISO8601($self->checkout_time) || "";
1209 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1213 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1214 $self->editor->requestor->id,
1222 $self->push_events($evt);
1233 $self->log_me("do_checkin()");
1236 return $self->bail_on_events(
1237 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1240 if( $self->checkin_check_holds_shelf() ) {
1241 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1242 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1243 $self->checkin_flesh_events;
1247 unless( $self->is_renewal ) {
1248 return $self->bail_on_events($self->editor->event)
1249 unless $self->editor->allowed('COPY_CHECKIN');
1252 $self->push_events($self->check_copy_alert());
1253 $self->push_events($self->check_checkin_copy_status());
1255 # the renew code will have already found our circulation object
1256 unless( $self->is_renewal and $self->circ ) {
1258 $self->editor->search_action_circulation(
1259 { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1262 # if the circ is marked as 'claims returned', add the event to the list
1263 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1264 if ($self->circ and $self->circ->stop_fines
1265 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1267 # handle the overridable events
1268 $self->override_events unless $self->is_renewal;
1269 return if $self->bail_out;
1273 $self->editor->search_action_transit_copy(
1274 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1278 $self->checkin_handle_circ;
1279 return if $self->bail_out;
1280 $self->checkin_changed(1);
1282 } elsif( $self->transit ) {
1283 my $hold_transit = $self->process_received_transit;
1284 $self->checkin_changed(1);
1286 if( $self->bail_out ) {
1287 $self->checkin_flesh_events;
1291 if( my $e = $self->check_checkin_copy_status() ) {
1292 # If the original copy status is special, alert the caller
1293 my $ev = $self->events;
1294 $self->events([$e]);
1295 $self->override_events;
1296 return if $self->bail_out;
1300 if( $hold_transit or
1301 $U->copy_status($self->copy->status)->id
1302 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1305 $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1306 $U->fetch_open_hold_by_copy($self->copy->id)
1309 $self->checkin_flesh_events;
1313 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1314 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1315 " that is in-transit, but there is no transit.. repairing");
1316 $self->reshelve_copy(1);
1317 return if $self->bail_out;
1320 if( $self->is_renewal ) {
1321 $self->push_events(OpenILS::Event->new('SUCCESS'));
1325 # ------------------------------------------------------------------------------
1326 # Circulations and transits are now closed where necessary. Now go on to see if
1327 # this copy can fulfill a hold or needs to be routed to a different location
1328 # ------------------------------------------------------------------------------
1330 if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1331 return if $self->bail_out;
1333 } else { # not needed for a hold
1336 my $circ_lib = (ref $self->copy->circ_lib) ?
1337 $self->copy->circ_lib->id : $self->copy->circ_lib;
1339 if( $self->remote_hold ) {
1340 $circ_lib = $self->remote_hold->pickup_lib;
1341 $logger->warn("circulator: Copy ".$self->copy->barcode.
1342 " is on a remote hold's shelf, sending to $circ_lib");
1345 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1347 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1349 $self->checkin_handle_precat();
1350 return if $self->bail_out;
1354 my $bc = $self->copy->barcode;
1355 $logger->info("circulator: copy $bc at a remote lib - sending home");
1356 $self->checkin_build_copy_transit();
1357 return if $self->bail_out;
1358 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1362 $self->reshelve_copy;
1363 return if $self->bail_out;
1365 unless($self->checkin_changed) {
1367 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1368 my $stat = $U->copy_status($self->copy->status)->id;
1370 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1371 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1372 $self->bail_out(1); # no need to commit anything
1375 $self->push_events(OpenILS::Event->new('SUCCESS'))
1376 unless @{$self->events};
1380 # ------------------------------------------------------------------------------
1381 # Update the patron penalty info in the DB
1382 # ------------------------------------------------------------------------------
1383 $U->update_patron_penalties(
1384 authtoken => $self->editor->authtoken,
1385 patron => $self->patron,
1386 background => 1 ) if $self->is_checkin;
1388 $self->checkin_flesh_events;
1394 my $force = $self->force || shift;
1395 my $copy = $self->copy;
1397 my $stat = $U->copy_status($copy->status)->id;
1400 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1401 $stat != OILS_COPY_STATUS_CATALOGING and
1402 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1403 $stat != OILS_COPY_STATUS_RESHELVING )) {
1405 $copy->status( OILS_COPY_STATUS_RESHELVING );
1407 $self->checkin_changed(1);
1412 # Returns true if the item is at the current location
1413 # because it was transited there for a hold and the
1414 # hold has not been fulfilled
1415 sub checkin_check_holds_shelf {
1417 return 0 unless $self->copy;
1420 $U->copy_status($self->copy->status)->id ==
1421 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1423 # find the hold that put us on the holds shelf
1424 my $holds = $self->editor->search_action_hold_request(
1426 current_copy => $self->copy->id,
1427 capture_time => { '!=' => undef },
1428 fulfillment_time => undef,
1429 cancel_time => undef,
1434 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1435 $self->reshelve_copy(1);
1439 my $hold = $$holds[0];
1441 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1442 $hold->id. "] for copy ".$self->copy->barcode);
1444 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1445 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1449 $logger->info("circulator: hold is not for here..");
1450 $self->remote_hold($hold);
1455 sub checkin_handle_precat {
1457 my $copy = $self->copy;
1459 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1460 $copy->status(OILS_COPY_STATUS_CATALOGING);
1461 $self->update_copy();
1462 $self->checkin_changed(1);
1463 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1468 sub checkin_build_copy_transit {
1470 my $copy = $self->copy;
1471 my $transit = Fieldmapper::action::transit_copy->new;
1473 $transit->source($self->editor->requestor->ws_ou);
1474 $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1475 $transit->target_copy($copy->id);
1476 $transit->source_send_time('now');
1477 $transit->copy_status( $U->copy_status($copy->status)->id );
1479 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1481 return $self->bail_on_events($self->editor->event)
1482 unless $self->editor->create_action_transit_copy($transit);
1484 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1486 $self->checkin_changed(1);
1490 sub attempt_checkin_hold_capture {
1492 my $copy = $self->copy;
1494 # See if this copy can fulfill any holds
1495 my ($hold) = $holdcode->find_nearest_permitted_hold(
1496 OpenSRF::AppSession->create('open-ils.storage'),
1497 $copy, $self->editor->requestor );
1500 $logger->debug("circulator: no potential permitted".
1501 "holds found for copy ".$copy->barcode);
1506 $logger->info("circulator: found permitted hold ".
1507 $hold->id . " for copy, capturing...");
1509 $hold->current_copy($copy->id);
1510 $hold->capture_time('now');
1512 # prevent DB errors caused by fetching
1513 # holds from storage, and updating through cstore
1514 $hold->clear_fulfillment_time;
1515 $hold->clear_fulfillment_staff;
1516 $hold->clear_fulfillment_lib;
1517 $hold->clear_expire_time;
1518 $hold->clear_cancel_time;
1519 $hold->clear_prev_check_time unless $hold->prev_check_time;
1521 $self->bail_on_events($self->editor->event)
1522 unless $self->editor->update_action_hold_request($hold);
1524 $self->checkin_changed(1);
1526 return 1 if $self->bail_out;
1528 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1530 # This hold was captured in the correct location
1531 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1532 $self->push_events(OpenILS::Event->new('SUCCESS'));
1534 #$self->do_hold_notify($hold->id);
1535 $self->notify_hold($hold->id);
1539 # Hold needs to be picked up elsewhere. Build a hold
1540 # transit and route the item.
1541 $self->checkin_build_hold_transit();
1542 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1543 return 1 if $self->bail_out;
1545 OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1548 # make sure we save the copy status
1553 sub do_hold_notify {
1554 my( $self, $holdid ) = @_;
1556 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1557 editor => $self->editor, hold_id => $holdid );
1559 if(!$notifier->event) {
1561 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1563 my $stat = $notifier->send_email_notify;
1564 if( $stat == '1' ) {
1565 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1566 $self->editor->commit;
1570 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1573 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1576 $self->editor->rollback;
1580 sub checkin_build_hold_transit {
1583 my $copy = $self->copy;
1584 my $hold = $self->hold;
1585 my $trans = Fieldmapper::action::hold_transit_copy->new;
1587 $logger->debug("circulator: building hold transit for ".$copy->barcode);
1589 $trans->hold($hold->id);
1590 $trans->source($self->editor->requestor->ws_ou);
1591 $trans->dest($hold->pickup_lib);
1592 $trans->source_send_time("now");
1593 $trans->target_copy($copy->id);
1595 # when the copy gets to its destination, it will recover
1596 # this status - put it onto the holds shelf
1597 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1599 return $self->bail_on_events($self->editor->event)
1600 unless $self->editor->create_action_hold_transit_copy($trans);
1605 sub process_received_transit {
1607 my $copy = $self->copy;
1608 my $copyid = $self->copy->id;
1610 my $status_name = $U->copy_status($copy->status)->name;
1611 $logger->debug("circulator: attempting transit receive on ".
1612 "copy $copyid. Copy status is $status_name");
1614 my $transit = $self->transit;
1616 if( $transit->dest != $self->editor->requestor->ws_ou ) {
1617 $logger->info("circulator: Fowarding transit on copy which is destined ".
1618 "for a different location. copy=$copyid,current ".
1619 "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1621 return $self->bail_on_events(
1622 OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1625 # The transit is received, set the receive time
1626 $transit->dest_recv_time('now');
1627 $self->bail_on_events($self->editor->event)
1628 unless $self->editor->update_action_transit_copy($transit);
1630 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1632 $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1633 $copy->status( $transit->copy_status );
1634 $self->update_copy();
1635 return if $self->bail_out;
1639 #$self->do_hold_notify($hold_transit->hold);
1640 $self->notify_hold($hold_transit->hold);
1645 OpenILS::Event->new(
1648 payload => { transit => $transit, holdtransit => $hold_transit } ));
1650 return $hold_transit;
1654 sub checkin_handle_circ {
1658 my $circ = $self->circ;
1659 my $copy = $self->copy;
1663 # backdate the circ if necessary
1664 if($self->backdate) {
1665 $self->checkin_handle_backdate;
1666 return if $self->bail_out;
1669 if(!$circ->stop_fines) {
1670 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1671 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1672 $circ->stop_fines_time('now') unless $self->backdate;
1673 $circ->stop_fines_time($self->backdate) if $self->backdate;
1676 # see if there are any fines owed on this circ. if not, close it
1677 $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1678 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1680 # Set the checkin vars since we have the item
1681 $circ->checkin_time('now');
1682 $circ->checkin_staff($self->editor->requestor->id);
1683 $circ->checkin_lib($self->editor->requestor->ws_ou);
1685 my $circ_lib = (ref $self->copy->circ_lib) ?
1686 $self->copy->circ_lib->id : $self->copy->circ_lib;
1687 my $stat = $U->copy_status($self->copy->status)->id;
1689 # If the item is lost/missing and it needs to be sent home, don't
1690 # reshelve the copy, leave it lost/missing so the recipient will know
1691 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1692 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1693 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1696 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1701 return $self->bail_on_events($self->editor->event)
1702 unless $self->editor->update_action_circulation($circ);
1706 sub checkin_handle_backdate {
1709 my $bd = $self->backdate;
1710 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1711 $bd = "${bd}T23:59:59";
1713 my $bills = $self->editor->search_money_billing(
1715 billing_ts => { '>=' => $bd },
1716 xact => $self->circ->id,
1717 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1721 for my $bill (@$bills) {
1722 if( !$bill->voided or $bill->voided =~ /f/i ) {
1724 my $n = $bill->note || "";
1725 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1727 $self->bail_on_events($self->editor->event)
1728 unless $self->editor->update_money_billing($bill);
1735 # XXX Legacy version for Circ.pm support
1736 sub _checkin_handle_backdate {
1737 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1740 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1741 $bd = "${bd}T23:59:59";
1744 my $bills = $session->request(
1745 "open-ils.storage.direct.money.billing.search_where.atomic",
1746 billing_ts => { '>=' => $bd },
1748 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1752 for my $bill (@$bills) {
1754 my $n = $bill->note || "";
1755 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1756 my $s = $session->request(
1757 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1758 return $U->DB_UPDATE_FAILED($bill) unless $s;
1768 sub find_patron_from_copy {
1770 my $circs = $self->editor->search_action_circulation(
1771 { target_copy => $self->copy->id, checkin_time => undef });
1772 my $circ = $circs->[0];
1773 return unless $circ;
1774 my $u = $self->editor->retrieve_actor_user($circ->usr)
1775 or return $self->bail_on_events($self->editor->event);
1779 sub check_checkin_copy_status {
1781 my $copy = $self->copy;
1787 my $status = $U->copy_status($copy->status)->id;
1790 if( $status == OILS_COPY_STATUS_AVAILABLE ||
1791 $status == OILS_COPY_STATUS_CHECKED_OUT ||
1792 $status == OILS_COPY_STATUS_IN_PROCESS ||
1793 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
1794 $status == OILS_COPY_STATUS_IN_TRANSIT ||
1795 $status == OILS_COPY_STATUS_CATALOGING ||
1796 $status == OILS_COPY_STATUS_RESHELVING );
1798 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1799 if( $status == OILS_COPY_STATUS_LOST );
1801 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1802 if( $status == OILS_COPY_STATUS_MISSING );
1804 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1809 # --------------------------------------------------------------------------
1810 # On checkin, we need to return as many relevant objects as we can
1811 # --------------------------------------------------------------------------
1812 sub checkin_flesh_events {
1815 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
1816 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1817 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1821 for my $evt (@{$self->events}) {
1824 $payload->{copy} = $U->unflesh_copy($self->copy);
1825 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1826 $payload->{circ} = $self->circ;
1827 $payload->{transit} = $self->transit;
1828 $payload->{hold} = $self->hold;
1830 $evt->{payload} = $payload;
1835 my( $self, $msg ) = @_;
1836 my $bc = ($self->copy) ? $self->copy->barcode :
1839 my $usr = ($self->patron) ? $self->patron->id : "";
1840 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1841 ", recipient=$usr, copy=$bc");
1847 $self->log_me("do_renew()");
1848 $self->is_renewal(1);
1850 unless( $self->is_renewal ) {
1851 return $self->bail_on_events($self->editor->events)
1852 unless $self->editor->allowed('RENEW_CIRC');
1855 # Make sure there is an open circ to renew that is not
1856 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1857 my $circ = $self->editor->search_action_circulation(
1858 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1861 $circ = $self->editor->search_action_circulation(
1863 target_copy => $self->copy->id,
1864 stop_fines => OILS_STOP_FINES_MAX_FINES,
1865 checkin_time => undef
1870 return $self->bail_on_events($self->editor->event) unless $circ;
1872 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1873 if $circ->renewal_remaining < 1;
1875 # -----------------------------------------------------------------
1877 $self->renewal_remaining( $circ->renewal_remaining - 1 );
1880 $self->run_renew_permit;
1883 $self->do_checkin();
1884 return if $self->bail_out;
1886 unless( $self->permit_override ) {
1888 return if $self->bail_out;
1889 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1890 $self->remove_event('ITEM_NOT_CATALOGED');
1893 $self->override_events;
1894 return if $self->bail_out;
1897 $self->do_checkout();
1902 my( $self, $evt ) = @_;
1903 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1904 $logger->debug("circulator: removing event from list: $evt");
1905 my @events = @{$self->events};
1906 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1911 my( $self, $evt ) = @_;
1912 $evt = (ref $evt) ? $evt->{textcode} : $evt;
1913 return grep { $_->{textcode} eq $evt } @{$self->events};
1918 sub run_renew_permit {
1920 my $runner = $self->script_runner;
1922 $runner->load($self->circ_permit_renew);
1923 my $result = $runner->run or
1924 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1925 my $events = $result->{events};
1927 $logger->activity("ciculator: circ_permit_renew for user ".
1928 $self->patron->id." returned events: @$events") if @$events;
1930 $self->push_events(OpenILS::Event->new($_)) for @$events;
1932 $logger->debug("circulator: re-creating script runner to be safe");
1933 $self->mk_script_runner;