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::AppSession;
6 use OpenSRF::Utils::SettingsClient;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Const qw/:const/;
9 use OpenILS::Application::AppUtils;
11 my $U = "OpenILS::Application::AppUtils";
15 my $legacy_script_support = 0;
19 flesh_fields => {acp => ['call_number'], acn => ['record']}
25 my $conf = OpenSRF::Utils::SettingsClient->new;
26 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
28 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
29 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
31 my $lb = $conf->config_value( @pfx2, 'script_path' );
32 $lb = [ $lb ] unless ref($lb);
35 return unless $legacy_script_support;
37 my @pfx = ( @pfx2, "scripts" );
38 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
39 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
40 my $d = $conf->config_value( @pfx, 'circ_duration' );
41 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
42 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
43 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
45 $logger->error( "Missing circ script(s)" )
46 unless( $p and $c and $d and $f and $m and $pr );
48 $scripts{circ_permit_patron} = $p;
49 $scripts{circ_permit_copy} = $c;
50 $scripts{circ_duration} = $d;
51 $scripts{circ_recurring_fines} = $f;
52 $scripts{circ_max_fines} = $m;
53 $scripts{circ_permit_renew} = $pr;
56 "circulator: Loaded rules scripts for circ: " .
57 "circ permit patron = $p, ".
58 "circ permit copy = $c, ".
59 "circ duration = $d, ".
60 "circ recurring fines = $f, " .
61 "circ max fines = $m, ".
62 "circ renew permit = $pr. ".
64 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
69 __PACKAGE__->register_method(
70 method => "run_method",
71 api_name => "open-ils.circ.checkout.permit",
73 Determines if the given checkout can occur
74 @param authtoken The login session key
75 @param params A trailing hash of named params including
76 barcode : The copy barcode,
77 patron : The patron the checkout is occurring for,
78 renew : true or false - whether or not this is a renewal
79 @return The event that occurred during the permit check.
83 __PACKAGE__->register_method (
84 method => 'run_method',
85 api_name => 'open-ils.circ.checkout.permit.override',
86 signature => q/@see open-ils.circ.checkout.permit/,
90 __PACKAGE__->register_method(
91 method => "run_method",
92 api_name => "open-ils.circ.checkout",
95 @param authtoken The login session key
96 @param params A named hash of params including:
98 barcode If no copy is provided, the copy is retrieved via barcode
99 copyid If no copy or barcode is provide, the copy id will be use
100 patron The patron's id
101 noncat True if this is a circulation for a non-cataloted item
102 noncat_type The non-cataloged type id
103 noncat_circ_lib The location for the noncat circ.
104 precat The item has yet to be cataloged
105 dummy_title The temporary title of the pre-cataloded item
106 dummy_author The temporary authr of the pre-cataloded item
107 Default is the home org of the staff member
108 @return The SUCCESS event on success, any other event depending on the error
111 __PACKAGE__->register_method(
112 method => "run_method",
113 api_name => "open-ils.circ.checkin",
116 Generic super-method for handling all copies
117 @param authtoken The login session key
118 @param params Hash of named parameters including:
119 barcode - The copy barcode
120 force - If true, copies in bad statuses will be checked in and give good statuses
121 noop - don't capture holds or put items into transit
122 void_overdues - void all overdues for the circulation (aka amnesty)
127 __PACKAGE__->register_method(
128 method => "run_method",
129 api_name => "open-ils.circ.checkin.override",
130 signature => q/@see open-ils.circ.checkin/
133 __PACKAGE__->register_method(
134 method => "run_method",
135 api_name => "open-ils.circ.renew.override",
136 signature => q/@see open-ils.circ.renew/,
140 __PACKAGE__->register_method(
141 method => "run_method",
142 api_name => "open-ils.circ.renew",
143 notes => <<" NOTES");
144 PARAMS( authtoken, circ => circ_id );
145 open-ils.circ.renew(login_session, circ_object);
146 Renews the provided circulation. login_session is the requestor of the
147 renewal and if the logged in user is not the same as circ->usr, then
148 the logged in user must have RENEW_CIRC permissions.
151 __PACKAGE__->register_method(
152 method => "run_method",
153 api_name => "open-ils.circ.checkout.full"
155 __PACKAGE__->register_method(
156 method => "run_method",
157 api_name => "open-ils.circ.checkout.full.override"
159 __PACKAGE__->register_method(
160 method => "run_method",
161 api_name => "open-ils.circ.reservation.pickup"
163 __PACKAGE__->register_method(
164 method => "run_method",
165 api_name => "open-ils.circ.reservation.return"
167 __PACKAGE__->register_method(
168 method => "run_method",
169 api_name => "open-ils.circ.checkout.inspect",
170 desc => q/Returns the circ matrix test result and, on success, the rule set and matrix test object/
175 my( $self, $conn, $auth, $args ) = @_;
176 translate_legacy_args($args);
177 my $api = $self->api_name;
180 OpenILS::Application::Circ::Circulator->new($auth, %$args);
182 return circ_events($circulator) if $circulator->bail_out;
184 # --------------------------------------------------------------------------
185 # First, check for a booking transit, as the barcode may not be a copy
186 # barcode, but a resource barcode, and nothing else in here will work
187 # --------------------------------------------------------------------------
189 if ((my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
190 my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
191 if (@$resources) { # yes!
193 my $res_id_list = [ map { $_->id } @$resources ];
194 my $transit = $circulator->editor->search_action_reservation_transit_copy(
196 { target_copy => $res_id_list, dest => $circulator->circ_lib, dest_recv_time => undef },
197 { order_by => { artc => 'source_send_time' }, limit => 1 }
199 )->[0]; # Any transit for this barcode?
201 if ($transit) { # yes! unwrap it.
203 my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
204 my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
206 my $success_event = new OpenILS::Event(
207 "SUCCESS", "payload" => {"reservation" => $reservation}
209 if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
210 if (my $copy = $circulator->editor->search_asset_copy([
211 { barcode => $bc, deleted => 'f' }, $MK_ENV_FLESH
212 ])->[0]) { # got a copy
213 $copy->status( $transit->copy_status );
214 $copy->editor($circulator->editor->requestor->id);
215 $copy->edit_date('now');
216 $circulator->editor->update_asset_copy($copy);
217 $success_event->{"payload"}->{"record"} =
218 $U->record_to_mvr($copy->call_number->record);
219 $copy->call_number($copy->call_number->id);
220 $success_event->{"payload"}->{"copy"} = $copy;
224 $transit->dest_recv_time('now');
225 $circulator->editor->update_action_reservation_transit_copy( $transit );
227 $circulator->editor->commit;
228 # Formerly this branch just stopped here. Argh!
229 $conn->respond_complete($success_event);
237 # --------------------------------------------------------------------------
238 # Go ahead and load the script runner to make sure we have all
239 # of the objects we need
240 # --------------------------------------------------------------------------
242 # XXX I wanted to make this better so it might support blocking renewals
243 # if a reservation has been placed on an item, but that will need more
244 # design, as institutions will differ in their policy on that. In the
245 # meantime making sure we're trying some kind of checkin will at least
246 # keep OPAC renewals from breaking since patrons don't have VIEW_USER...
248 $circulator->is_res_checkin($circulator->is_checkin(1))
249 if $api =~ /reservation.return/ or (
250 $api =~ /checkin/ and $circulator->seems_like_reservation()
253 $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
255 $circulator->is_renewal(1) if $api =~ /renew/;
256 $circulator->is_checkin(1) if $api =~ /checkin/;
258 $circulator->mk_env();
259 $circulator->noop if $circulator->claims_never_checked_out;
261 if($legacy_script_support and not $circulator->is_checkin) {
262 $circulator->mk_script_runner();
263 $circulator->legacy_script_support(1);
264 $circulator->circ_permit_patron($scripts{circ_permit_patron});
265 $circulator->circ_permit_copy($scripts{circ_permit_copy});
266 $circulator->circ_duration($scripts{circ_duration});
267 $circulator->circ_permit_renew($scripts{circ_permit_renew});
269 return circ_events($circulator) if $circulator->bail_out;
272 $circulator->override(1) if $api =~ /override/o;
274 if( $api =~ /checkout\.permit/ ) {
275 $circulator->do_permit();
277 } elsif( $api =~ /checkout.full/ ) {
279 # requesting a precat checkout implies that any required
280 # overrides have been performed. Go ahead and re-override.
281 $circulator->skip_permit_key(1);
282 $circulator->override(1) if $circulator->request_precat;
283 $circulator->do_permit();
284 $circulator->is_checkout(1);
285 unless( $circulator->bail_out ) {
286 $circulator->events([]);
287 $circulator->do_checkout();
290 } elsif( $circulator->is_res_checkout ) {
291 $circulator->do_reservation_pickup();
293 } elsif( $api =~ /inspect/ ) {
294 my $data = $circulator->do_inspect();
295 $circulator->editor->rollback;
298 } elsif( $api =~ /checkout/ ) {
299 $circulator->is_checkout(1);
300 $circulator->do_checkout();
302 } elsif( $circulator->is_res_checkin ) {
303 $circulator->do_reservation_return();
304 $circulator->do_checkin() if ($circulator->copy());
305 } elsif( $api =~ /checkin/ ) {
306 $circulator->do_checkin();
308 } elsif( $api =~ /renew/ ) {
309 $circulator->is_renewal(1);
310 $circulator->do_renew();
313 if( $circulator->bail_out ) {
316 # make sure no success event accidentally slip in
318 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
321 my @e = @{$circulator->events};
322 push( @ee, $_->{textcode} ) for @e;
323 $logger->info("circulator: bailing out with events: " . (join ", ", @ee));
325 $circulator->editor->rollback;
328 $circulator->editor->commit;
331 $circulator->script_runner->cleanup if $circulator->script_runner;
333 $conn->respond_complete(circ_events($circulator));
335 unless($circulator->bail_out) {
336 $circulator->do_hold_notify($circulator->notify_hold)
337 if $circulator->notify_hold;
338 $circulator->retarget_holds if $circulator->retarget;
339 $circulator->append_reading_list;
340 $circulator->make_trigger_events;
346 my @e = @{$circ->events};
347 # if we have multiple events, SUCCESS should not be one of them;
348 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
349 return (@e == 1) ? $e[0] : \@e;
353 sub translate_legacy_args {
356 if( $$args{barcode} ) {
357 $$args{copy_barcode} = $$args{barcode};
358 delete $$args{barcode};
361 if( $$args{copyid} ) {
362 $$args{copy_id} = $$args{copyid};
363 delete $$args{copyid};
366 if( $$args{patronid} ) {
367 $$args{patron_id} = $$args{patronid};
368 delete $$args{patronid};
371 if( $$args{patron} and !ref($$args{patron}) ) {
372 $$args{patron_id} = $$args{patron};
373 delete $$args{patron};
377 if( $$args{noncat} ) {
378 $$args{is_noncat} = $$args{noncat};
379 delete $$args{noncat};
382 if( $$args{precat} ) {
383 $$args{is_precat} = $$args{request_precat} = $$args{precat};
384 delete $$args{precat};
390 # --------------------------------------------------------------------------
391 # This package actually manages all of the circulation logic
392 # --------------------------------------------------------------------------
393 package OpenILS::Application::Circ::Circulator;
394 use strict; use warnings;
395 use vars q/$AUTOLOAD/;
397 use OpenILS::Utils::Fieldmapper;
398 use OpenSRF::Utils::Cache;
399 use Digest::MD5 qw(md5_hex);
400 use DateTime::Format::ISO8601;
401 use OpenILS::Utils::PermitHold;
402 use OpenSRF::Utils qw/:datetime/;
403 use OpenSRF::Utils::SettingsClient;
404 use OpenILS::Application::Circ::Holds;
405 use OpenILS::Application::Circ::Transit;
406 use OpenSRF::Utils::Logger qw(:logger);
407 use OpenILS::Utils::CStoreEditor qw/:funcs/;
408 use OpenILS::Application::Circ::ScriptBuilder;
409 use OpenILS::Const qw/:const/;
410 use OpenILS::Utils::Penalty;
411 use OpenILS::Application::Circ::CircCommon;
414 my $holdcode = "OpenILS::Application::Circ::Holds";
415 my $transcode = "OpenILS::Application::Circ::Transit";
421 # --------------------------------------------------------------------------
422 # Add a pile of automagic getter/setter methods
423 # --------------------------------------------------------------------------
424 my @AUTOLOAD_FIELDS = qw/
471 recurring_fines_level
483 cancelled_hold_transit
490 circ_matrix_matchpoint
492 legacy_script_support
502 claims_never_checked_out
511 my $type = ref($self) or die "$self is not an object";
513 my $name = $AUTOLOAD;
516 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
517 $logger->error("circulator: $type: invalid autoload field: $name");
518 die "$type: invalid autoload field: $name\n"
523 *{"${type}::${name}"} = sub {
526 $s->{$name} = $v if defined $v;
530 return $self->$name($data);
535 my( $class, $auth, %args ) = @_;
536 $class = ref($class) || $class;
537 my $self = bless( {}, $class );
540 $self->editor(new_editor(xact => 1, authtoken => $auth));
542 unless( $self->editor->checkauth ) {
543 $self->bail_on_events($self->editor->event);
547 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
549 $self->$_($args{$_}) for keys %args;
552 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
554 # if this is a renewal, default to desk_renewal
555 $self->desk_renewal(1) unless
556 $self->opac_renewal or $self->phone_renewal or $self->sip_renewal;
558 $self->capture('') unless $self->capture;
560 unless(%user_groups) {
561 my $gps = $self->editor->retrieve_all_permission_grp_tree;
562 %user_groups = map { $_->id => $_ } @$gps;
569 # --------------------------------------------------------------------------
570 # True if we should discontinue processing
571 # --------------------------------------------------------------------------
573 my( $self, $bool ) = @_;
574 if( defined $bool ) {
575 $logger->info("circulator: BAILING OUT") if $bool;
576 $self->{bail_out} = $bool;
578 return $self->{bail_out};
583 my( $self, @evts ) = @_;
586 $e->{payload} = $self->copy if
587 ($e->{textcode} eq 'COPY_NOT_AVAILABLE');
589 $logger->info("circulator: pushing event ".$e->{textcode});
590 push( @{$self->events}, $e ) unless
591 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
597 return '' if $self->skip_permit_key;
598 my $key = md5_hex( time() . rand() . "$$" );
599 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
600 return $self->permit_key($key);
603 sub check_permit_key {
605 return 1 if $self->skip_permit_key;
606 my $key = $self->permit_key;
607 return 0 unless $key;
608 my $k = "oils_permit_key_$key";
609 my $one = $self->cache_handle->get_cache($k);
610 $self->cache_handle->delete_cache($k);
611 return ($one) ? 1 : 0;
614 sub seems_like_reservation {
617 # Some words about the following method:
618 # 1) It requires the VIEW_USER permission, but that's not an
619 # issue, right, since all staff should have that?
620 # 2) It returns only one reservation at a time, even if an item can be
621 # and is currently overbooked. Hmmm....
622 my $booking_ses = create OpenSRF::AppSession("open-ils.booking");
623 my $result = $booking_ses->request(
624 "open-ils.booking.reservations.by_returnable_resource_barcode",
625 $self->editor->authtoken,
628 $booking_ses->disconnect;
630 return $self->bail_on_events($result) if defined $U->event_code($result);
633 $self->reservation(shift @$result);
641 # save_trimmed_copy() used just to be a block in mk_env(), but was separated for re-use
642 sub save_trimmed_copy {
643 my ($self, $copy) = @_;
646 $self->volume($copy->call_number);
647 $self->title($self->volume->record);
648 $self->copy->call_number($self->volume->id);
649 $self->volume->record($self->title->id);
650 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
651 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
652 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
653 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
659 my $e = $self->editor;
661 # --------------------------------------------------------------------------
662 # Grab the fleshed copy
663 # --------------------------------------------------------------------------
664 unless($self->is_noncat) {
667 $copy = $e->retrieve_asset_copy(
668 [$self->copy_id, $MK_ENV_FLESH ]) or return $e->event;
670 } elsif( $self->copy_barcode ) {
672 $copy = $e->search_asset_copy(
673 [{barcode => $self->copy_barcode, deleted => 'f'}, $MK_ENV_FLESH ])->[0];
674 } elsif( $self->reservation ) {
675 my $res = $e->json_query(
677 "select" => {"acp" => ["id"]},
682 "field" => "barcode",
686 "field" => "current_resource"
694 "id" => (ref $self->reservation) ?
695 $self->reservation->id : $self->reservation
700 if (ref $res eq "ARRAY" and scalar @$res) {
701 $logger->info("circulator: mapped reservation " .
702 $self->reservation . " to copy " . $res->[0]->{"id"});
703 $copy = $e->retrieve_asset_copy([$res->[0]->{"id"}, $MK_ENV_FLESH]);
708 $self->save_trimmed_copy($copy);
710 # We can't renew if there is no copy
711 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
712 if $self->is_renewal;
717 # --------------------------------------------------------------------------
719 # --------------------------------------------------------------------------
723 flesh_fields => {au => [ qw/ card / ]}
726 if( $self->patron_id ) {
727 $patron = $e->retrieve_actor_user([$self->patron_id, $flesh])
728 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
730 } elsif( $self->patron_barcode ) {
732 # note: throwing ACTOR_USER_NOT_FOUND instead of ACTOR_CARD_NOT_FOUND is intentional
733 my $card = $e->search_actor_card({barcode => $self->patron_barcode})->[0]
734 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
736 $patron = $e->search_actor_user([{card => $card->id}, $flesh])->[0]
737 or return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'));
740 if( my $copy = $self->copy ) {
741 my $circs = $e->search_action_circulation(
742 {target_copy => $copy->id, checkin_time => undef});
744 if( my $circ = $circs->[0] ) {
745 $patron = $e->retrieve_actor_user([$circ->usr, $flesh])
751 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
752 unless $self->patron($patron) or $self->is_checkin;
754 unless($self->is_checkin) {
756 # Check for inactivity and patron reg. expiration
758 $self->bail_on_events(OpenILS::Event->new('PATRON_INACTIVE'))
759 unless $U->is_true($patron->active);
761 $self->bail_on_events(OpenILS::Event->new('PATRON_CARD_INACTIVE'))
762 unless $U->is_true($patron->card->active);
764 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
765 cleanse_ISO8601($patron->expire_date));
767 $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
768 if( CORE::time > $expire->epoch ) ;
772 # --------------------------------------------------------------------------
773 # This builds the script runner environment and fetches most of the
775 # --------------------------------------------------------------------------
776 sub mk_script_runner {
782 qw/copy copy_barcode copy_id patron
783 patron_id patron_barcode volume title editor/;
785 # Translate our objects into the ScriptBuilder args hash
786 $$args{$_} = $self->$_() for @fields;
788 $args->{ignore_user_status} = 1 if $self->is_checkin;
789 $$args{fetch_patron_by_circ_copy} = 1;
790 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
792 if( my $pco = $self->pending_checkouts ) {
793 $logger->info("circulator: we were given a pending checkouts number of $pco");
794 $$args{patronItemsOut} = $pco;
797 # This fetches most of the objects we need
798 $self->script_runner(
799 OpenILS::Application::Circ::ScriptBuilder->build($args));
801 # Now we translate the ScriptBuilder objects back into self
802 $self->$_($$args{$_}) for @fields;
804 my @evts = @{$args->{_events}} if $args->{_events};
806 $logger->debug("circulator: script builder returned events: @evts") if @evts;
810 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
811 if(!$self->is_noncat and
813 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
817 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
818 return $self->bail_on_events(@e);
823 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
824 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
825 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
826 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
830 # We can't renew if there is no copy
831 return $self->bail_on_events(@evts) if
832 $self->is_renewal and !$self->copy;
834 # Set some circ-specific flags in the script environment
835 my $evt = "environment";
836 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
838 if( $self->is_noncat ) {
839 $self->script_runner->insert("$evt.isNonCat", 1);
840 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
843 if( $self->is_precat ) {
844 $self->script_runner->insert("environment.isPrecat", 1, 1);
847 $self->script_runner->add_path( $_ ) for @$script_libs;
852 # --------------------------------------------------------------------------
853 # Does the circ permit work
854 # --------------------------------------------------------------------------
858 $self->log_me("do_permit()");
860 unless( $self->editor->requestor->id == $self->patron->id ) {
861 return $self->bail_on_events($self->editor->event)
862 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
865 $self->check_captured_holds();
866 $self->do_copy_checks();
867 return if $self->bail_out;
868 $self->run_patron_permit_scripts();
869 $self->run_copy_permit_scripts()
870 unless $self->is_precat or $self->is_noncat;
871 $self->check_item_deposit_events();
872 $self->override_events();
873 return if $self->bail_out;
875 if($self->is_precat and not $self->request_precat) {
878 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
879 return $self->bail_out(1) unless $self->is_renewal;
883 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
886 sub check_item_deposit_events {
888 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
889 if $self->is_deposit and not $self->is_deposit_exempt;
890 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
891 if $self->is_rental and not $self->is_rental_exempt;
894 # returns true if the user is not required to pay deposits
895 sub is_deposit_exempt {
897 my $pid = (ref $self->patron->profile) ?
898 $self->patron->profile->id : $self->patron->profile;
899 my $groups = $U->ou_ancestor_setting_value(
900 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
901 for my $grp (@$groups) {
902 return 1 if $self->is_group_descendant($grp, $pid);
907 # returns true if the user is not required to pay rental fees
908 sub is_rental_exempt {
910 my $pid = (ref $self->patron->profile) ?
911 $self->patron->profile->id : $self->patron->profile;
912 my $groups = $U->ou_ancestor_setting_value(
913 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
914 for my $grp (@$groups) {
915 return 1 if $self->is_group_descendant($grp, $pid);
920 sub is_group_descendant {
921 my($self, $p_id, $c_id) = @_;
922 return 0 unless defined $p_id and defined $c_id;
923 return 1 if $c_id == $p_id;
924 while(my $grp = $user_groups{$c_id}) {
925 $c_id = $grp->parent;
926 return 0 unless defined $c_id;
927 return 1 if $c_id == $p_id;
932 sub check_captured_holds {
934 my $copy = $self->copy;
935 my $patron = $self->patron;
937 return undef unless $copy;
939 my $s = $U->copy_status($copy->status)->id;
940 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
941 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
943 # Item is on the holds shelf, make sure it's going to the right person
944 my $holds = $self->editor->search_action_hold_request(
947 current_copy => $copy->id ,
948 capture_time => { '!=' => undef },
949 cancel_time => undef,
950 fulfillment_time => undef
956 if( $holds and $$holds[0] ) {
957 return undef if $$holds[0]->usr == $patron->id;
960 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
962 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
968 my $copy = $self->copy;
971 my $stat = $U->copy_status($copy->status)->id;
973 # We cannot check out a copy if it is in-transit
974 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
975 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
978 $self->handle_claims_returned();
979 return if $self->bail_out;
981 # no claims returned circ was found, check if there is any open circ
982 unless( $self->is_renewal ) {
984 my $circs = $self->editor->search_action_circulation(
985 { target_copy => $copy->id, checkin_time => undef }
988 if(my $old_circ = $circs->[0]) { # an open circ was found
990 my $payload = {copy => $copy};
992 if($old_circ->usr == $self->patron->id) {
994 $payload->{old_circ} = $old_circ;
996 # If there is an open circulation on the checkout item and an auto-renew
997 # interval is defined, inform the caller that they should go
998 # ahead and renew the item instead of warning about open circulations.
1000 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
1001 $self->editor->requestor->ws_ou,
1002 'circ.checkout_auto_renew_age',
1006 if($auto_renew_intvl) {
1007 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
1008 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
1010 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
1011 $payload->{auto_renew} = 1;
1016 return $self->bail_on_events(
1017 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
1023 my $LEGACY_CIRC_EVENT_MAP = {
1024 'no_item' => 'ITEM_NOT_CATALOGED',
1025 'actor.usr.barred' => 'PATRON_BARRED',
1026 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1027 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
1028 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1029 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
1030 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1031 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
1032 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
1033 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
1037 # ---------------------------------------------------------------------
1038 # This pushes any patron-related events into the list but does not
1039 # set bail_out for any events
1040 # ---------------------------------------------------------------------
1041 sub run_patron_permit_scripts {
1043 my $runner = $self->script_runner;
1044 my $patronid = $self->patron->id;
1048 if(!$self->legacy_script_support) {
1050 my $results = $self->run_indb_circ_test;
1051 unless($self->circ_test_success) {
1052 # no_item result is OK during noncat checkout
1053 unless(@$results == 1 && $results->[0]->{fail_part} eq 'no_item' and $self->is_noncat) {
1054 push @allevents, $self->matrix_test_result_events;
1060 # ---------------------------------------------------------------------
1061 # # Now run the patron permit script
1062 # ---------------------------------------------------------------------
1063 $runner->load($self->circ_permit_patron);
1064 my $result = $runner->run or
1065 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
1067 my $patron_events = $result->{events};
1069 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1070 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
1071 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
1072 $penalties = $penalties->{fatal_penalties};
1074 for my $pen (@$penalties) {
1075 my $event = OpenILS::Event->new($pen->name);
1076 $event->{desc} = $pen->label;
1077 push(@allevents, $event);
1080 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
1084 $_->{payload} = $self->copy if
1085 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1088 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
1090 $self->push_events(@allevents);
1093 sub matrix_test_result_codes {
1095 map { $_->{"fail_part"} } @{$self->matrix_test_result};
1098 sub matrix_test_result_events {
1101 my $event = new OpenILS::Event(
1102 $LEGACY_CIRC_EVENT_MAP->{$_->{"fail_part"}} || $_->{"fail_part"}
1104 $event->{"payload"} = {"fail_part" => $_->{"fail_part"}};
1106 } (@{$self->matrix_test_result});
1109 sub run_indb_circ_test {
1111 return $self->matrix_test_result if $self->matrix_test_result;
1113 my $dbfunc = ($self->is_renewal) ?
1114 'action.item_user_renew_test' : 'action.item_user_circ_test';
1116 if( $self->is_precat && $self->request_precat) {
1117 $self->make_precat_copy;
1118 return if $self->bail_out;
1121 my $results = $self->editor->json_query(
1124 $self->editor->requestor->ws_ou,
1125 ($self->is_noncat or ($self->is_precat and !$self->override and !$self->is_renewal)) ? undef : $self->copy->id,
1131 $self->circ_test_success($U->is_true($results->[0]->{success}));
1133 if(my $mp = $results->[0]->{matchpoint}) {
1134 $logger->info("circulator: circ policy test found matchpoint $mp");
1135 $self->circ_matrix_matchpoint(
1136 $self->editor->retrieve_config_circ_matrix_matchpoint([
1139 flesh_fields => {ccmm =>
1140 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
1146 return $self->matrix_test_result($results);
1149 # ---------------------------------------------------------------------
1150 # given a use and copy, this will calculate the circulation policy
1151 # parameters. Only works with in-db circ.
1152 # ---------------------------------------------------------------------
1156 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
1158 $self->run_indb_circ_test;
1161 circ_test_success => $self->circ_test_success,
1162 failure_events => [],
1163 failure_codes => [],
1164 matchpoint => $self->circ_matrix_matchpoint
1167 unless($self->circ_test_success) {
1168 $results->{"failure_codes"} = [ $self->matrix_test_result_codes ];
1169 $results->{"failure_events"} = [ $self->matrix_test_result_events ];
1172 if($self->circ_matrix_matchpoint) {
1173 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
1174 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
1175 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
1177 my $policy = $self->get_circ_policy(
1178 $duration_rule, $recurring_fine_rule, $max_fine_rule);
1180 $$results{$_} = $$policy{$_} for keys %$policy;
1186 # ---------------------------------------------------------------------
1187 # Loads the circ policy info for duration, recurring fine, and max
1188 # fine based on the current copy
1189 # ---------------------------------------------------------------------
1190 sub get_circ_policy {
1191 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
1194 duration_rule => $duration_rule->name,
1195 recurring_fine_rule => $recurring_fine_rule->name,
1196 max_fine_rule => $max_fine_rule->name,
1197 max_fine => $self->get_max_fine_amount($max_fine_rule),
1198 fine_interval => $recurring_fine_rule->recurrence_interval,
1199 renewal_remaining => $duration_rule->max_renewals
1202 $policy->{duration} = $duration_rule->shrt
1203 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1204 $policy->{duration} = $duration_rule->normal
1205 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1206 $policy->{duration} = $duration_rule->extended
1207 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1209 $policy->{recurring_fine} = $recurring_fine_rule->low
1210 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1211 $policy->{recurring_fine} = $recurring_fine_rule->normal
1212 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1213 $policy->{recurring_fine} = $recurring_fine_rule->high
1214 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1219 sub get_max_fine_amount {
1221 my $max_fine_rule = shift;
1222 my $max_amount = $max_fine_rule->amount;
1224 # if is_percent is true then the max->amount is
1225 # use as a percentage of the copy price
1226 if ($U->is_true($max_fine_rule->is_percent)) {
1227 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1228 $max_amount = $price * $max_fine_rule->amount / 100;
1230 $U->ou_ancestor_setting_value(
1232 'circ.max_fine.cap_at_price',
1236 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1237 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1245 sub run_copy_permit_scripts {
1247 my $copy = $self->copy || return;
1248 my $runner = $self->script_runner;
1252 if(!$self->legacy_script_support) {
1253 my $results = $self->run_indb_circ_test;
1254 push @allevents, $self->matrix_test_result_events
1255 unless $self->circ_test_success;
1258 # ---------------------------------------------------------------------
1259 # Capture all of the copy permit events
1260 # ---------------------------------------------------------------------
1261 $runner->load($self->circ_permit_copy);
1262 my $result = $runner->run or
1263 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1264 my $copy_events = $result->{events};
1266 # ---------------------------------------------------------------------
1267 # Now collect all of the events together
1268 # ---------------------------------------------------------------------
1269 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1272 # See if this copy has an alert message
1273 my $ae = $self->check_copy_alert();
1274 push( @allevents, $ae ) if $ae;
1276 # uniquify the events
1277 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1278 @allevents = values %hash;
1280 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1282 $self->push_events(@allevents);
1286 sub check_copy_alert {
1288 return undef if $self->is_renewal;
1289 return OpenILS::Event->new(
1290 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1291 if $self->copy and $self->copy->alert_message;
1297 # --------------------------------------------------------------------------
1298 # If the call is overriding and has permissions to override every collected
1299 # event, the are cleared. Any event that the caller does not have
1300 # permission to override, will be left in the event list and bail_out will
1302 # XXX We need code in here to cancel any holds/transits on copies
1303 # that are being force-checked out
1304 # --------------------------------------------------------------------------
1305 sub override_events {
1307 my @events = @{$self->events};
1308 return unless @events;
1310 if(!$self->override) {
1311 return $self->bail_out(1)
1312 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1317 for my $e (@events) {
1318 my $tc = $e->{textcode};
1319 next if $tc eq 'SUCCESS';
1320 my $ov = "$tc.override";
1321 $logger->info("circulator: attempting to override event: $ov");
1323 return $self->bail_on_events($self->editor->event)
1324 unless( $self->editor->allowed($ov) );
1329 # --------------------------------------------------------------------------
1330 # If there is an open claimsreturn circ on the requested copy, close the
1331 # circ if overriding, otherwise bail out
1332 # --------------------------------------------------------------------------
1333 sub handle_claims_returned {
1335 my $copy = $self->copy;
1337 my $CR = $self->editor->search_action_circulation(
1339 target_copy => $copy->id,
1340 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1341 checkin_time => undef,
1345 return unless ($CR = $CR->[0]);
1349 # - If the caller has set the override flag, we will check the item in
1350 if($self->override) {
1352 $CR->checkin_time('now');
1353 $CR->checkin_scan_time('now');
1354 $CR->checkin_lib($self->editor->requestor->ws_ou);
1355 $CR->checkin_workstation($self->editor->requestor->wsid);
1356 $CR->checkin_staff($self->editor->requestor->id);
1358 $evt = $self->editor->event
1359 unless $self->editor->update_action_circulation($CR);
1362 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1365 $self->bail_on_events($evt) if $evt;
1370 # --------------------------------------------------------------------------
1371 # This performs the checkout
1372 # --------------------------------------------------------------------------
1376 $self->log_me("do_checkout()");
1378 # make sure perms are good if this isn't a renewal
1379 unless( $self->is_renewal ) {
1380 return $self->bail_on_events($self->editor->event)
1381 unless( $self->editor->allowed('COPY_CHECKOUT') );
1384 # verify the permit key
1385 unless( $self->check_permit_key ) {
1386 if( $self->permit_override ) {
1387 return $self->bail_on_events($self->editor->event)
1388 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1390 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1394 # if this is a non-cataloged circ, build the circ and finish
1395 if( $self->is_noncat ) {
1396 $self->checkout_noncat;
1398 OpenILS::Event->new('SUCCESS',
1399 payload => { noncat_circ => $self->circ }));
1403 if( $self->is_precat ) {
1404 $self->make_precat_copy;
1405 return if $self->bail_out;
1407 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1408 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1411 $self->do_copy_checks;
1412 return if $self->bail_out;
1414 $self->run_checkout_scripts();
1415 return if $self->bail_out;
1417 $self->build_checkout_circ_object();
1418 return if $self->bail_out;
1420 my $modify_to_start = $self->booking_adjusted_due_date();
1421 return if $self->bail_out;
1423 $self->apply_modified_due_date($modify_to_start);
1424 return if $self->bail_out;
1426 return $self->bail_on_events($self->editor->event)
1427 unless $self->editor->create_action_circulation($self->circ);
1429 # refresh the circ to force local time zone for now
1430 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1432 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1434 return if $self->bail_out;
1436 $self->apply_deposit_fee();
1437 return if $self->bail_out;
1439 $self->handle_checkout_holds();
1440 return if $self->bail_out;
1442 # ------------------------------------------------------------------------------
1443 # Update the patron penalty info in the DB. Run it for permit-overrides
1444 # since the penalties are not updated during the permit phase
1445 # ------------------------------------------------------------------------------
1446 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1448 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1451 if($self->is_renewal) {
1452 # flesh the billing summary for the checked-in circ
1453 $pcirc = $self->editor->retrieve_action_circulation([
1455 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1460 OpenILS::Event->new('SUCCESS',
1462 copy => $U->unflesh_copy($self->copy),
1463 circ => $self->circ,
1465 holds_fulfilled => $self->fulfilled_holds,
1466 deposit_billing => $self->deposit_billing,
1467 rental_billing => $self->rental_billing,
1468 parent_circ => $pcirc,
1469 patron => ($self->return_patron) ? $self->patron : undef,
1470 patron_money => $self->editor->retrieve_money_user_summary($self->patron->id)
1476 sub apply_deposit_fee {
1478 my $copy = $self->copy;
1480 ($self->is_deposit and not $self->is_deposit_exempt) or
1481 ($self->is_rental and not $self->is_rental_exempt);
1483 return if $self->is_deposit and $self->skip_deposit_fee;
1484 return if $self->is_rental and $self->skip_rental_fee;
1486 my $bill = Fieldmapper::money::billing->new;
1487 my $amount = $copy->deposit_amount;
1491 if($self->is_deposit) {
1492 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1494 $self->deposit_billing($bill);
1496 $billing_type = OILS_BILLING_TYPE_RENTAL;
1498 $self->rental_billing($bill);
1501 $bill->xact($self->circ->id);
1502 $bill->amount($amount);
1503 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1504 $bill->billing_type($billing_type);
1505 $bill->btype($btype);
1506 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1508 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1513 my $copy = $self->copy;
1515 my $stat = $copy->status if ref $copy->status;
1516 my $loc = $copy->location if ref $copy->location;
1517 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1519 $copy->status($stat->id) if $stat;
1520 $copy->location($loc->id) if $loc;
1521 $copy->circ_lib($circ_lib->id) if $circ_lib;
1522 $copy->editor($self->editor->requestor->id);
1523 $copy->edit_date('now');
1524 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1526 return $self->bail_on_events($self->editor->event)
1527 unless $self->editor->update_asset_copy($self->copy);
1529 $copy->status($U->copy_status($copy->status));
1530 $copy->location($loc) if $loc;
1531 $copy->circ_lib($circ_lib) if $circ_lib;
1534 sub update_reservation {
1536 my $reservation = $self->reservation;
1538 my $usr = $reservation->usr;
1539 my $target_rt = $reservation->target_resource_type;
1540 my $target_r = $reservation->target_resource;
1541 my $current_r = $reservation->current_resource;
1543 $reservation->usr($usr->id) if ref $usr;
1544 $reservation->target_resource_type($target_rt->id) if ref $target_rt;
1545 $reservation->target_resource($target_r->id) if ref $target_r;
1546 $reservation->current_resource($current_r->id) if ref $current_r;
1548 return $self->bail_on_events($self->editor->event)
1549 unless $self->editor->update_booking_reservation($self->reservation);
1552 ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
1553 $self->reservation($reservation);
1557 sub bail_on_events {
1558 my( $self, @evts ) = @_;
1559 $self->push_events(@evts);
1564 # ------------------------------------------------------------------------------
1565 # When an item is checked out, see if we can fulfill a hold for this patron
1566 # ------------------------------------------------------------------------------
1567 sub handle_checkout_holds {
1569 my $copy = $self->copy;
1570 my $patron = $self->patron;
1572 my $e = $self->editor;
1573 $self->fulfilled_holds([]);
1575 # pre/non-cats can't fulfill a hold
1576 return if $self->is_precat or $self->is_noncat;
1578 my $hold = $e->search_action_hold_request({
1579 current_copy => $copy->id ,
1580 cancel_time => undef,
1581 fulfillment_time => undef,
1583 {expire_time => undef},
1584 {expire_time => {'>' => 'now'}}
1588 if($hold and $hold->usr != $patron->id) {
1589 # reset the hold since the copy is now checked out
1591 $logger->info("circulator: un-targeting hold ".$hold->id.
1592 " because copy ".$copy->id." is getting checked out");
1594 $hold->clear_prev_check_time;
1595 $hold->clear_current_copy;
1596 $hold->clear_capture_time;
1598 return $self->bail_on_event($e->event)
1599 unless $e->update_action_hold_request($hold);
1605 $hold = $self->find_related_user_hold($copy, $patron) or return;
1606 $logger->info("circulator: found related hold to fulfill in checkout");
1609 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1611 # if the hold was never officially captured, capture it.
1612 $hold->current_copy($copy->id);
1613 $hold->capture_time('now') unless $hold->capture_time;
1614 $hold->fulfillment_time('now');
1615 $hold->fulfillment_staff($e->requestor->id);
1616 $hold->fulfillment_lib($e->requestor->ws_ou);
1618 return $self->bail_on_events($e->event)
1619 unless $e->update_action_hold_request($hold);
1621 $holdcode->delete_hold_copy_maps($e, $hold->id);
1622 return $self->fulfilled_holds([$hold->id]);
1626 # ------------------------------------------------------------------------------
1627 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1628 # the patron directly targets the checked out item, see if there is another hold
1629 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1630 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1631 # ------------------------------------------------------------------------------
1632 sub find_related_user_hold {
1633 my($self, $copy, $patron) = @_;
1634 my $e = $self->editor;
1636 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1638 return undef unless $U->ou_ancestor_setting_value(
1639 $e->requestor->ws_ou, 'circ.checkout_fills_related_hold', $e);
1641 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1643 select => {ahr => ['id']},
1648 fkey => 'current_copy',
1649 type => 'left' # there may be no current_copy
1656 fulfillment_time => undef,
1657 cancel_time => undef,
1659 {expire_time => undef},
1660 {expire_time => {'>' => 'now'}}
1667 target => $self->volume->id
1673 target => $self->title->id
1679 {id => undef}, # left-join copy may be nonexistent
1680 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1684 order_by => {ahr => {request_time => {direction => 'asc'}}},
1688 my $hold_info = $e->json_query($args)->[0];
1689 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1694 sub run_checkout_scripts {
1699 my $runner = $self->script_runner;
1708 if(!$self->legacy_script_support) {
1709 $self->run_indb_circ_test();
1710 $duration = $self->circ_matrix_matchpoint->duration_rule;
1711 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1712 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1716 $runner->load($self->circ_duration);
1718 my $result = $runner->run or
1719 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1721 $duration_name = $result->{durationRule};
1722 $recurring_name = $result->{recurringFinesRule};
1723 $max_fine_name = $result->{maxFine};
1726 $duration_name = $duration->name if $duration;
1727 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1730 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1731 return $self->bail_on_events($evt) if ($evt && !$nobail);
1733 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1734 return $self->bail_on_events($evt) if ($evt && !$nobail);
1736 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1737 return $self->bail_on_events($evt) if ($evt && !$nobail);
1742 # The item circulates with an unlimited duration
1748 $self->duration_rule($duration);
1749 $self->recurring_fines_rule($recurring);
1750 $self->max_fine_rule($max_fine);
1754 sub build_checkout_circ_object {
1757 my $circ = Fieldmapper::action::circulation->new;
1758 my $duration = $self->duration_rule;
1759 my $max = $self->max_fine_rule;
1760 my $recurring = $self->recurring_fines_rule;
1761 my $copy = $self->copy;
1762 my $patron = $self->patron;
1766 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1768 my $dname = $duration->name;
1769 my $mname = $max->name;
1770 my $rname = $recurring->name;
1772 $logger->debug("circulator: building circulation ".
1773 "with duration=$dname, maxfine=$mname, recurring=$rname");
1775 $circ->duration($policy->{duration});
1776 $circ->recurring_fine($policy->{recurring_fine});
1777 $circ->duration_rule($duration->name);
1778 $circ->recurring_fine_rule($recurring->name);
1779 $circ->max_fine_rule($max->name);
1780 $circ->max_fine($policy->{max_fine});
1781 $circ->fine_interval($recurring->recurrence_interval);
1782 $circ->renewal_remaining($duration->max_renewals);
1786 $logger->info("circulator: copy found with an unlimited circ duration");
1787 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1788 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1789 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1790 $circ->renewal_remaining(0);
1793 $circ->target_copy( $copy->id );
1794 $circ->usr( $patron->id );
1795 $circ->circ_lib( $self->circ_lib );
1796 $circ->workstation($self->editor->requestor->wsid)
1797 if defined $self->editor->requestor->wsid;
1799 # renewals maintain a link to the parent circulation
1800 $circ->parent_circ($self->parent_circ);
1802 if( $self->is_renewal ) {
1803 $circ->opac_renewal('t') if $self->opac_renewal;
1804 $circ->phone_renewal('t') if $self->phone_renewal;
1805 $circ->desk_renewal('t') if $self->desk_renewal;
1806 $circ->renewal_remaining($self->renewal_remaining);
1807 $circ->circ_staff($self->editor->requestor->id);
1811 # if the user provided an overiding checkout time,
1812 # (e.g. the checkout really happened several hours ago), then
1813 # we apply that here. Does this need a perm??
1814 $circ->xact_start(cleanse_ISO8601($self->checkout_time))
1815 if $self->checkout_time;
1817 # if a patron is renewing, 'requestor' will be the patron
1818 $circ->circ_staff($self->editor->requestor->id);
1819 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1824 sub do_reservation_pickup {
1827 $self->log_me("do_reservation_pickup()");
1829 $self->reservation->pickup_time('now');
1832 $self->reservation->current_resource &&
1833 $self->reservation->current_resource->catalog_item
1835 $self->copy( $self->reservation->current_resource->catalog_item );
1836 $self->patron( $self->reservation->usr );
1837 $self->run_checkout_scripts(1);
1839 my $duration = $self->duration_rule;
1840 my $max = $self->max_fine_rule;
1841 my $recurring = $self->recurring_fines_rule;
1843 if ($duration && $max && $recurring) {
1844 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1846 my $dname = $duration->name;
1847 my $mname = $max->name;
1848 my $rname = $recurring->name;
1850 $logger->debug("circulator: building reservation ".
1851 "with duration=$dname, maxfine=$mname, recurring=$rname");
1853 $self->reservation->fine_amount($policy->{recurring_fine});
1854 $self->reservation->max_fine($policy->{max_fine});
1855 $self->reservation->fine_interval($recurring->recurrence_interval);
1858 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1859 $self->update_copy();
1862 $self->reservation->fine_amount($self->reservation->fine_amount);
1863 $self->reservation->max_fine($self->reservation->max_fine);
1864 $self->reservation->fine_interval($self->reservation->fine_interval);
1867 $self->update_reservation();
1870 sub do_reservation_return {
1872 my $request = shift;
1874 $self->log_me("do_reservation_return()");
1876 if (not ref $self->reservation) {
1877 my ($reservation, $evt) =
1878 $U->fetch_booking_reservation($self->reservation);
1879 return $self->bail_on_events($evt) if $evt;
1880 $self->reservation($reservation);
1883 $self->generate_fines(1);
1884 $self->reservation->return_time('now');
1885 $self->update_reservation();
1886 $self->reshelve_copy if $self->copy;
1888 if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
1889 $self->copy( $self->reservation->current_resource->catalog_item );
1893 sub booking_adjusted_due_date {
1895 my $circ = $self->circ;
1896 my $copy = $self->copy;
1901 if( $self->due_date ) {
1903 return $self->bail_on_events($self->editor->event)
1904 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1906 $circ->due_date(cleanse_ISO8601($self->due_date));
1910 return unless $copy and $circ->due_date;
1913 my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
1914 if (@$booking_items) {
1915 my $booking_item = $booking_items->[0];
1916 my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
1918 my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
1919 my $shorten_circ_setting = $resource_type->elbow_room ||
1920 $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
1923 my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
1924 my $bookings = $booking_ses->request(
1925 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
1926 { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date, fields => { cancel_time => undef }}
1928 $booking_ses->disconnect;
1930 my $dt_parser = DateTime::Format::ISO8601->new;
1931 my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
1933 for my $bid (@$bookings) {
1935 my $booking = $self->editor->retrieve_booking_reservation( $bid );
1937 my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
1938 my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
1940 return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
1941 if ($booking_start < DateTime->now);
1944 if ($U->is_true($stop_circ_setting)) {
1945 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') );
1947 $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
1948 $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now);
1951 # We set the circ duration here only to affect the logic that will
1952 # later (in a DB trigger) mangle the time part of the due date to
1953 # 11:59pm. Having any circ duration that is not a whole number of
1954 # days is enough to prevent the "correction."
1955 my $new_circ_duration = $due_date->epoch - time;
1956 $new_circ_duration++ if $new_circ_duration % 86400 == 0;
1957 $circ->duration("$new_circ_duration seconds");
1959 $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
1963 return $self->bail_on_events($self->editor->event)
1964 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1970 sub apply_modified_due_date {
1972 my $shift_earlier = shift;
1973 my $circ = $self->circ;
1974 my $copy = $self->copy;
1976 if( $self->due_date ) {
1978 return $self->bail_on_events($self->editor->event)
1979 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1981 $circ->due_date(cleanse_ISO8601($self->due_date));
1985 # if the due_date lands on a day when the location is closed
1986 return unless $copy and $circ->due_date;
1988 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1990 # due-date overlap should be determined by the location the item
1991 # is checked out from, not the owning or circ lib of the item
1992 my $org = $self->editor->requestor->ws_ou;
1994 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1995 " with an item due date of ".$circ->due_date );
1997 my $dateinfo = $U->storagereq(
1998 'open-ils.storage.actor.org_unit.closed_date.overlap',
1999 $org, $circ->due_date );
2002 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
2003 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
2005 # XXX make the behavior more dynamic
2006 # for now, we just push the due date to after the close date
2007 if ($shift_earlier) {
2008 $circ->due_date($dateinfo->{start});
2010 $circ->due_date($dateinfo->{end});
2018 sub create_due_date {
2019 my( $self, $duration ) = @_;
2021 # if there is a raw time component (e.g. from postgres),
2022 # turn it into an interval that interval_to_seconds can parse
2023 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
2025 # for now, use the server timezone. TODO: use workstation org timezone
2026 my $due_date = DateTime->now(time_zone => 'local');
2028 # add the circ duration
2029 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
2031 # return ISO8601 time with timezone
2032 return $due_date->strftime('%FT%T%z');
2037 sub make_precat_copy {
2039 my $copy = $self->copy;
2042 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
2044 $copy->editor($self->editor->requestor->id);
2045 $copy->edit_date('now');
2046 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
2047 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
2048 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
2049 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
2050 $self->update_copy();
2054 $logger->info("circulator: Creating a new precataloged ".
2055 "copy in checkout with barcode " . $self->copy_barcode);
2057 $copy = Fieldmapper::asset::copy->new;
2058 $copy->circ_lib($self->circ_lib);
2059 $copy->creator($self->editor->requestor->id);
2060 $copy->editor($self->editor->requestor->id);
2061 $copy->barcode($self->copy_barcode);
2062 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
2063 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
2064 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
2066 $copy->dummy_title($self->dummy_title || "");
2067 $copy->dummy_author($self->dummy_author || "");
2068 $copy->dummy_isbn($self->dummy_isbn || "");
2069 $copy->circ_modifier($self->circ_modifier);
2072 # See if we need to override the circ_lib for the copy with a configured circ_lib
2073 # Setting is shortname of the org unit
2074 my $precat_circ_lib = $U->ou_ancestor_setting_value(
2075 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
2077 if($precat_circ_lib) {
2078 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
2081 $self->bail_on_events($self->editor->event);
2085 $copy->circ_lib($org->id);
2089 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
2091 $self->push_events($self->editor->event);
2095 # this is a little bit of a hack, but we need to
2096 # get the copy into the script runner
2097 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
2101 sub checkout_noncat {
2107 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
2108 my $count = $self->noncat_count || 1;
2109 my $cotime = cleanse_ISO8601($self->checkout_time) || "";
2111 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
2115 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
2116 $self->editor->requestor->id,
2124 $self->push_events($evt);
2135 $self->log_me("do_checkin()");
2137 return $self->bail_on_events(
2138 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
2141 if( $self->checkin_check_holds_shelf() ) {
2142 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
2143 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
2144 $self->checkin_flesh_events;
2148 unless( $self->is_renewal ) {
2149 return $self->bail_on_events($self->editor->event)
2150 unless $self->editor->allowed('COPY_CHECKIN');
2153 $self->push_events($self->check_copy_alert());
2154 $self->push_events($self->check_checkin_copy_status());
2156 # the renew code will have already found our circulation object
2157 unless( $self->is_renewal and $self->circ ) {
2158 my $circs = $self->editor->search_action_circulation(
2159 { target_copy => $self->copy->id, checkin_time => undef });
2160 $self->circ($$circs[0]);
2162 # for now, just warn if there are multiple open circs on a copy
2163 $logger->warn("circulator: we have ".scalar(@$circs).
2164 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
2167 # run the fine generator against this circ, if this circ is there
2168 $self->generate_fines if ($self->circ);
2170 # if the circ is marked as 'claims returned', add the event to the list
2171 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
2172 if ($self->circ and $self->circ->stop_fines
2173 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
2175 $self->check_circ_deposit();
2177 # handle the overridable events
2178 $self->override_events unless $self->is_renewal;
2179 return if $self->bail_out;
2183 $self->editor->search_action_transit_copy(
2184 { target_copy => $self->copy->id, dest_recv_time => undef }
2190 $self->checkin_handle_circ;
2191 return if $self->bail_out;
2192 $self->checkin_changed(1);
2194 } elsif( $self->transit ) {
2195 my $hold_transit = $self->process_received_transit;
2196 $self->checkin_changed(1);
2198 if( $self->bail_out ) {
2199 $self->checkin_flesh_events;
2203 if( my $e = $self->check_checkin_copy_status() ) {
2204 # If the original copy status is special, alert the caller
2205 my $ev = $self->events;
2206 $self->events([$e]);
2207 $self->override_events;
2208 return if $self->bail_out;
2212 if( $hold_transit or
2213 $U->copy_status($self->copy->status)->id
2214 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2217 if( $hold_transit ) {
2218 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2220 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
2225 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
2227 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
2228 $self->reshelve_copy(1);
2229 $self->cancelled_hold_transit(1);
2230 $self->notify_hold(0); # don't notify for cancelled holds
2231 return if $self->bail_out;
2235 # hold transited to correct location
2236 $self->checkin_flesh_events;
2241 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
2243 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
2244 " that is in-transit, but there is no transit.. repairing");
2245 $self->reshelve_copy(1);
2246 return if $self->bail_out;
2249 if( $self->is_renewal ) {
2250 $self->push_events(OpenILS::Event->new('SUCCESS'));
2254 # ------------------------------------------------------------------------------
2255 # Circulations and transits are now closed where necessary. Now go on to see if
2256 # this copy can fulfill a hold or needs to be routed to a different location
2257 # ------------------------------------------------------------------------------
2259 my $needed_for_something = 0; # formerly "needed_for_hold"
2261 if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
2263 if (!$self->remote_hold) {
2264 my $potential_hold = $self->hold_capture_is_possible;
2265 my $potential_reservation = $self->reservation_capture_is_possible;
2267 if ($potential_hold and $potential_reservation) {
2268 $logger->info("circulator: item could fulfill either hold or reservation");
2269 $self->push_events(new OpenILS::Event(
2270 "HOLD_RESERVATION_CONFLICT",
2271 "hold" => $potential_hold,
2272 "reservation" => $potential_reservation
2274 return if $self->bail_out;
2275 } elsif ($potential_hold) {
2276 $needed_for_something =
2277 $self->attempt_checkin_hold_capture;
2278 } elsif ($potential_reservation) {
2279 $needed_for_something =
2280 $self->attempt_checkin_reservation_capture;
2283 return if $self->bail_out;
2285 unless($needed_for_something) {
2286 my $circ_lib = (ref $self->copy->circ_lib) ?
2287 $self->copy->circ_lib->id : $self->copy->circ_lib;
2289 if( $self->remote_hold ) {
2290 $circ_lib = $self->remote_hold->pickup_lib;
2291 $logger->warn("circulator: Copy ".$self->copy->barcode.
2292 " is on a remote hold's shelf, sending to $circ_lib");
2295 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
2297 if( $circ_lib == $self->editor->requestor->ws_ou ) {
2298 # copy is where it needs to be, either for hold or reshelving
2300 $self->checkin_handle_precat();
2301 return if $self->bail_out;
2304 # copy needs to transit "home", or stick here if it's a floating copy
2306 if ($U->is_true( $self->copy->floating ) && !$self->remote_hold) { # copy is floating, stick here
2307 $self->checkin_changed(1);
2308 $self->copy->circ_lib( $self->editor->requestor->ws_ou );
2311 my $bc = $self->copy->barcode;
2312 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
2313 $self->checkin_build_copy_transit($circ_lib);
2314 return if $self->bail_out;
2315 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
2319 } else { # no-op checkin
2320 if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
2321 $self->checkin_changed(1);
2322 $self->copy->circ_lib( $self->editor->requestor->ws_ou );
2326 $logger->info("LFW XXX: way down here"); # LFW XXX
2328 if($self->claims_never_checked_out and
2329 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
2331 # the item was not supposed to be checked out to the user and should now be marked as missing
2332 $self->copy->status(OILS_COPY_STATUS_MISSING);
2336 $self->reshelve_copy unless $needed_for_something;
2339 return if $self->bail_out;
2341 unless($self->checkin_changed) {
2343 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
2344 my $stat = $U->copy_status($self->copy->status)->id;
2346 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
2347 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
2348 $self->bail_out(1); # no need to commit anything
2352 $self->push_events(OpenILS::Event->new('SUCCESS'))
2353 unless @{$self->events};
2356 OpenILS::Utils::Penalty->calculate_penalties(
2357 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
2359 $self->checkin_flesh_events;
2363 # if a deposit was payed for this item, push the event
2364 sub check_circ_deposit {
2366 return unless $self->circ;
2367 my $deposit = $self->editor->search_money_billing(
2369 xact => $self->circ->id,
2371 }, {idlist => 1})->[0];
2373 $self->push_events(OpenILS::Event->new(
2374 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
2379 my $force = $self->force || shift;
2380 my $copy = $self->copy;
2382 my $stat = $U->copy_status($copy->status)->id;
2385 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
2386 $stat != OILS_COPY_STATUS_CATALOGING and
2387 $stat != OILS_COPY_STATUS_IN_TRANSIT and
2388 $stat != OILS_COPY_STATUS_RESHELVING )) {
2390 $copy->status( OILS_COPY_STATUS_RESHELVING );
2392 $self->checkin_changed(1);
2397 # Returns true if the item is at the current location
2398 # because it was transited there for a hold and the
2399 # hold has not been fulfilled
2400 sub checkin_check_holds_shelf {
2402 return 0 unless $self->copy;
2405 $U->copy_status($self->copy->status)->id ==
2406 OILS_COPY_STATUS_ON_HOLDS_SHELF;
2408 # find the hold that put us on the holds shelf
2409 my $holds = $self->editor->search_action_hold_request(
2411 current_copy => $self->copy->id,
2412 capture_time => { '!=' => undef },
2413 fulfillment_time => undef,
2414 cancel_time => undef,
2419 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
2420 $self->reshelve_copy(1);
2424 my $hold = $$holds[0];
2426 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2427 $hold->id. "] for copy ".$self->copy->barcode);
2429 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2430 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2434 $logger->info("circulator: hold is not for here..");
2435 $self->remote_hold($hold);
2440 sub checkin_handle_precat {
2442 my $copy = $self->copy;
2444 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2445 $copy->status(OILS_COPY_STATUS_CATALOGING);
2446 $self->update_copy();
2447 $self->checkin_changed(1);
2448 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2453 sub checkin_build_copy_transit {
2456 my $copy = $self->copy;
2457 my $transit = Fieldmapper::action::transit_copy->new;
2459 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2460 $logger->info("circulator: transiting copy to $dest");
2462 $transit->source($self->editor->requestor->ws_ou);
2463 $transit->dest($dest);
2464 $transit->target_copy($copy->id);
2465 $transit->source_send_time('now');
2466 $transit->copy_status( $U->copy_status($copy->status)->id );
2468 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2470 return $self->bail_on_events($self->editor->event)
2471 unless $self->editor->create_action_transit_copy($transit);
2473 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2475 $self->checkin_changed(1);
2479 sub hold_capture_is_possible {
2481 my $copy = $self->copy;
2483 # we've been explicitly told not to capture any holds
2484 return 0 if $self->capture eq 'nocapture';
2486 # See if this copy can fulfill any holds
2487 my $hold = $holdcode->find_nearest_permitted_hold(
2488 $self->editor, $copy, $self->editor->requestor, 1 # check_only
2490 return undef if ref $hold eq "HASH" and
2491 $hold->{"textcode"} eq "ACTION_HOLD_REQUEST_NOT_FOUND";
2495 sub reservation_capture_is_possible {
2497 my $copy = $self->copy;
2499 # we've been explicitly told not to capture any holds
2500 return 0 if $self->capture eq 'nocapture';
2502 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2503 my $resv = $booking_ses->request(
2504 "open-ils.booking.reservations.could_capture",
2505 $self->editor->authtoken, $copy->barcode
2507 $booking_ses->disconnect;
2508 if (ref($resv) eq "HASH" and exists $resv->{"textcode"}) {
2509 $self->push_events($resv);
2515 # returns true if the item was used (or may potentially be used
2516 # in subsequent calls) to capture a hold.
2517 sub attempt_checkin_hold_capture {
2519 my $copy = $self->copy;
2521 # we've been explicitly told not to capture any holds
2522 return 0 if $self->capture eq 'nocapture';
2524 # See if this copy can fulfill any holds
2525 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2526 $self->editor, $copy, $self->editor->requestor );
2529 $logger->debug("circulator: no potential permitted".
2530 "holds found for copy ".$copy->barcode);
2534 if($self->capture ne 'capture') {
2535 # see if this item is in a hold-capture-delay location
2536 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2537 if($U->is_true($location->hold_verify)) {
2538 $self->bail_on_events(
2539 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2544 $self->retarget($retarget);
2546 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2548 $hold->current_copy($copy->id);
2549 $hold->capture_time('now');
2550 $self->put_hold_on_shelf($hold)
2551 if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2553 # prevent DB errors caused by fetching
2554 # holds from storage, and updating through cstore
2555 $hold->clear_fulfillment_time;
2556 $hold->clear_fulfillment_staff;
2557 $hold->clear_fulfillment_lib;
2558 $hold->clear_expire_time;
2559 $hold->clear_cancel_time;
2560 $hold->clear_prev_check_time unless $hold->prev_check_time;
2562 $self->bail_on_events($self->editor->event)
2563 unless $self->editor->update_action_hold_request($hold);
2565 $self->checkin_changed(1);
2567 return 0 if $self->bail_out;
2569 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2571 # This hold was captured in the correct location
2572 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2573 $self->push_events(OpenILS::Event->new('SUCCESS'));
2575 #$self->do_hold_notify($hold->id);
2576 $self->notify_hold($hold->id);
2580 # Hold needs to be picked up elsewhere. Build a hold
2581 # transit and route the item.
2582 $self->checkin_build_hold_transit();
2583 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2584 return 0 if $self->bail_out;
2585 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2588 # make sure we save the copy status
2593 sub attempt_checkin_reservation_capture {
2595 my $copy = $self->copy;
2597 # we've been explicitly told not to capture any holds
2598 return 0 if $self->capture eq 'nocapture';
2600 my $booking_ses = OpenSRF::AppSession->connect("open-ils.booking");
2601 my $evt = $booking_ses->request(
2602 "open-ils.booking.resources.capture_for_reservation",
2603 $self->editor->authtoken,
2605 1 # don't update copy - we probably have it locked
2607 $booking_ses->disconnect;
2609 if (ref($evt) ne "HASH" or not exists $evt->{"textcode"}) {
2611 "open-ils.booking.resources.capture_for_reservation " .
2612 "didn't return an event!"
2616 $evt->{"textcode"} eq "RESERVATION_NOT_FOUND" and
2617 $evt->{"payload"}->{"fail_cause"} eq "not-transferable"
2619 # not-transferable is an error event we'll pass on the user
2620 $logger->warn("reservation capture attempted against non-transferable item");
2621 $self->push_events($evt);
2623 } elsif ($evt->{"textcode"} eq "SUCCESS") {
2624 # Re-retrieve copy as reservation capture may have changed
2625 # its status and whatnot.
2627 "circulator: booking capture win on copy " . $self->copy->id
2629 if (my $new_copy_status = $evt->{"payload"}->{"new_copy_status"}) {
2631 "circulator: changing copy " . $self->copy->id .
2632 "'s status from " . $self->copy->status . " to " .
2635 $self->copy->status($new_copy_status);
2638 $self->reservation($evt->{"payload"}->{"reservation"});
2640 if (exists $evt->{"payload"}->{"transit"}) {
2644 "org" => $evt->{"payload"}->{"transit"}->dest
2648 $self->checkin_changed(1);
2652 # other results are treated as "nothing to capture"
2656 sub do_hold_notify {
2657 my( $self, $holdid ) = @_;
2659 my $e = new_editor(xact => 1);
2660 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2662 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2663 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2665 $logger->info("circulator: running delayed hold notify process");
2667 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2668 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2670 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2671 hold_id => $holdid, requestor => $self->editor->requestor);
2673 $logger->debug("circulator: built hold notifier");
2675 if(!$notifier->event) {
2677 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2679 my $stat = $notifier->send_email_notify;
2680 if( $stat == '1' ) {
2681 $logger->info("circulator: hold notify succeeded for hold $holdid");
2685 $logger->warn("circulator: * hold notify failed for hold $holdid");
2688 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2692 sub retarget_holds {
2694 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2695 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2696 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2697 # no reason to wait for the return value
2701 sub checkin_build_hold_transit {
2704 my $copy = $self->copy;
2705 my $hold = $self->hold;
2706 my $trans = Fieldmapper::action::hold_transit_copy->new;
2708 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2710 $trans->hold($hold->id);
2711 $trans->source($self->editor->requestor->ws_ou);
2712 $trans->dest($hold->pickup_lib);
2713 $trans->source_send_time("now");
2714 $trans->target_copy($copy->id);
2716 # when the copy gets to its destination, it will recover
2717 # this status - put it onto the holds shelf
2718 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2720 return $self->bail_on_events($self->editor->event)
2721 unless $self->editor->create_action_hold_transit_copy($trans);
2726 sub process_received_transit {
2728 my $copy = $self->copy;
2729 my $copyid = $self->copy->id;
2731 my $status_name = $U->copy_status($copy->status)->name;
2732 $logger->debug("circulator: attempting transit receive on ".
2733 "copy $copyid. Copy status is $status_name");
2735 my $transit = $self->transit;
2737 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2738 # - this item is in-transit to a different location
2740 my $tid = $transit->id;
2741 my $loc = $self->editor->requestor->ws_ou;
2742 my $dest = $transit->dest;
2744 $logger->info("circulator: Fowarding transit on copy which is destined ".
2745 "for a different location. transit=$tid, copy=$copyid, current ".
2746 "location=$loc, destination location=$dest");
2748 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2750 # grab the associated hold object if available
2751 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2752 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2754 return $self->bail_on_events($evt);
2757 # The transit is received, set the receive time
2758 $transit->dest_recv_time('now');
2759 $self->bail_on_events($self->editor->event)
2760 unless $self->editor->update_action_transit_copy($transit);
2762 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2764 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2765 $copy->status( $transit->copy_status );
2766 $self->update_copy();
2767 return if $self->bail_out;
2771 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2773 # hold has arrived at destination, set shelf time
2774 $self->put_hold_on_shelf($hold);
2775 $self->bail_on_events($self->editor->event)
2776 unless $self->editor->update_action_hold_request($hold);
2777 return if $self->bail_out;
2779 $self->notify_hold($hold_transit->hold);
2784 OpenILS::Event->new(
2787 payload => { transit => $transit, holdtransit => $hold_transit } ));
2789 return $hold_transit;
2793 # ------------------------------------------------------------------
2794 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2795 # ------------------------------------------------------------------
2796 sub put_hold_on_shelf {
2797 my($self, $hold) = @_;
2799 $hold->shelf_time('now');
2801 my $shelf_expire = $U->ou_ancestor_setting_value(
2802 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2805 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2806 my $expire_time = DateTime->now->add(seconds => $seconds);
2807 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2815 sub generate_fines {
2817 my $reservation = shift;
2821 my $id = $reservation ? $self->reservation->id : $self->circ->id;
2823 my $st = OpenSRF::AppSession->connect('open-ils.storage');
2826 'open-ils.storage.action.circulation.overdue.generate_fines',
2833 # refresh the circ in case the fine generator set the stop_fines field
2834 $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
2835 $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
2840 sub checkin_handle_circ {
2842 my $circ = $self->circ;
2843 my $copy = $self->copy;
2847 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2849 # backdate the circ if necessary
2850 if($self->backdate) {
2851 $self->checkin_handle_backdate;
2852 return if $self->bail_out;
2855 if($self->void_overdues) {
2856 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2857 $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2858 return $self->bail_on_events($evt) if $evt;
2861 if(!$circ->stop_fines) {
2862 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2863 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2864 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2865 $circ->stop_fines_time('now');
2866 $circ->stop_fines_time($self->backdate) if $self->backdate;
2869 # Set the checkin vars since we have the item
2870 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2872 # capture the true scan time for back-dated checkins
2873 $circ->checkin_scan_time('now');
2875 $circ->checkin_staff($self->editor->requestor->id);
2876 $circ->checkin_lib($self->editor->requestor->ws_ou);
2877 $circ->checkin_workstation($self->editor->requestor->wsid);
2879 my $circ_lib = (ref $self->copy->circ_lib) ?
2880 $self->copy->circ_lib->id : $self->copy->circ_lib;
2881 my $stat = $U->copy_status($self->copy->status)->id;
2883 # immediately available keeps items lost or missing items from going home before being handled
2884 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2885 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2888 if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2890 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2891 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2893 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2897 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2899 $self->checkin_handle_lost($circ_lib);
2903 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2908 # see if there are any fines owed on this circ. if not, close it
2909 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2910 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2912 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2914 return $self->bail_on_events($self->editor->event)
2915 unless $self->editor->update_action_circulation($circ);
2917 # make sure the circ isn't closed if we just voided some fines
2918 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2919 return $self->bail_on_events($evt) if $evt;
2925 # ------------------------------------------------------------------
2926 # See if we need to void billings for lost checkin
2927 # ------------------------------------------------------------------
2928 sub checkin_handle_lost {
2930 my $circ_lib = shift;
2931 my $circ = $self->circ;
2933 my $max_return = $U->ou_ancestor_setting_value(
2934 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2939 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2940 $tm[5] -= 1 if $tm[5] > 0;
2941 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2943 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2944 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2946 $max_return = 0 if $today < $last_chance;
2949 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2951 my $void_lost = $U->ou_ancestor_setting_value(
2952 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2953 my $void_lost_fee = $U->ou_ancestor_setting_value(
2954 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2955 my $restore_od = $U->ou_ancestor_setting_value(
2956 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2958 $self->checkin_handle_lost_now_found(3) if $void_lost;
2959 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2960 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2963 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2968 sub checkin_handle_backdate {
2971 my $bd = cleanse_ISO8601($self->backdate);
2973 # ------------------------------------------------------------------
2974 # clean up the backdate for date comparison
2975 # we want any bills created on or after the backdate
2976 # ------------------------------------------------------------------
2977 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
2978 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
2979 $bd = cleanse_ISO8601($new_date->ymd . 'T' . $original_date->strftime('%T%z'));
2981 $self->backdate($bd);
2983 my $bills = $self->editor->search_money_billing(
2985 billing_ts => { '>=' => $bd },
2986 xact => $self->circ->id,
2991 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2993 for my $bill (@$bills) {
2994 unless( $U->is_true($bill->voided) ) {
2995 $logger->info("backdate voiding bill ".$bill->id);
2997 $bill->void_time('now');
2998 $bill->voider($self->editor->requestor->id);
2999 my $n = $bill->note || "";
3000 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
3002 $self->bail_on_events($self->editor->event)
3003 unless $self->editor->update_money_billing($bill);
3011 sub find_patron_from_copy {
3013 my $circs = $self->editor->search_action_circulation(
3014 { target_copy => $self->copy->id, checkin_time => undef });
3015 my $circ = $circs->[0];
3016 return unless $circ;
3017 my $u = $self->editor->retrieve_actor_user($circ->usr)
3018 or return $self->bail_on_events($self->editor->event);
3022 sub check_checkin_copy_status {
3024 my $copy = $self->copy;
3026 my $status = $U->copy_status($copy->status)->id;
3029 if( $status == OILS_COPY_STATUS_AVAILABLE ||
3030 $status == OILS_COPY_STATUS_CHECKED_OUT ||
3031 $status == OILS_COPY_STATUS_IN_PROCESS ||
3032 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
3033 $status == OILS_COPY_STATUS_IN_TRANSIT ||
3034 $status == OILS_COPY_STATUS_CATALOGING ||
3035 $status == OILS_COPY_STATUS_ON_RESV_SHELF ||
3036 $status == OILS_COPY_STATUS_RESHELVING );
3038 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
3039 if( $status == OILS_COPY_STATUS_LOST );
3041 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
3042 if( $status == OILS_COPY_STATUS_MISSING );
3044 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
3049 # --------------------------------------------------------------------------
3050 # On checkin, we need to return as many relevant objects as we can
3051 # --------------------------------------------------------------------------
3052 sub checkin_flesh_events {
3055 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
3056 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
3057 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
3060 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
3063 if($self->hold and !$self->hold->cancel_time) {
3064 $hold = $self->hold;
3065 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
3069 # if we checked in a circulation, flesh the billing summary data
3070 $self->circ->billable_transaction(
3071 $self->editor->retrieve_money_billable_transaction([
3073 {flesh => 1, flesh_fields => {mbt => ['summary']}}
3079 # flesh some patron fields before returning
3081 $self->editor->retrieve_actor_user([
3086 au => ['card', 'billing_address', 'mailing_address']
3093 for my $evt (@{$self->events}) {
3096 $payload->{copy} = $U->unflesh_copy($self->copy);
3097 $payload->{record} = $record,
3098 $payload->{circ} = $self->circ;
3099 $payload->{transit} = $self->transit;
3100 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
3101 $payload->{hold} = $hold;
3102 $payload->{patron} = $self->patron;
3103 $payload->{reservation} = $self->reservation
3104 unless (not $self->reservation or $self->reservation->cancel_time);
3106 $evt->{payload} = $payload;
3111 my( $self, $msg ) = @_;
3112 my $bc = ($self->copy) ? $self->copy->barcode :
3115 my $usr = ($self->patron) ? $self->patron->id : "";
3116 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
3117 ", recipient=$usr, copy=$bc");
3123 $self->log_me("do_renew()");
3125 # Make sure there is an open circ to renew that is not
3126 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
3127 my $usrid = $self->patron->id if $self->patron;
3130 # If we have a patron, match them to the circ
3131 $circ = $self->editor->search_action_circulation(
3132 {target_copy => $self->copy->id, usr => $usrid, stop_fines => undef})->[0];
3134 $circ = $self->editor->search_action_circulation(
3135 {target_copy => $self->copy->id, stop_fines => undef})->[0];
3140 $circ = $self->editor->search_action_circulation(
3141 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
3143 $circ = $self->editor->search_action_circulation(
3144 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
3148 return $self->bail_on_events($self->editor->event) unless $circ;
3150 # A user is not allowed to renew another user's items without permission
3151 unless( $circ->usr eq $self->editor->requestor->id ) {
3152 return $self->bail_on_events($self->editor->events)
3153 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
3156 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
3157 if $circ->renewal_remaining < 1;
3159 # -----------------------------------------------------------------
3161 $self->parent_circ($circ->id);
3162 $self->renewal_remaining( $circ->renewal_remaining - 1 );
3165 $self->run_renew_permit;
3168 $self->do_checkin();
3169 return if $self->bail_out;
3171 unless( $self->permit_override ) {
3173 return if $self->bail_out;
3174 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
3175 $self->remove_event('ITEM_NOT_CATALOGED');
3178 $self->override_events;
3179 return if $self->bail_out;
3182 $self->do_checkout();
3187 my( $self, $evt ) = @_;
3188 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3189 $logger->debug("circulator: removing event from list: $evt");
3190 my @events = @{$self->events};
3191 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
3196 my( $self, $evt ) = @_;
3197 $evt = (ref $evt) ? $evt->{textcode} : $evt;
3198 return grep { $_->{textcode} eq $evt } @{$self->events};
3203 sub run_renew_permit {
3206 if ($U->ou_ancestor_setting_value($self->circ_lib, 'circ.block_renews_for_holds')) {
3207 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
3208 $self->editor, $self->copy, $self->editor->requestor, 1
3210 $self->push_events(new OpenILS::Event("COPY_NEEDED_FOR_HOLD")) if $hold;
3213 if(!$self->legacy_script_support) {
3214 my $results = $self->run_indb_circ_test;
3215 $self->push_events($self->matrix_test_result_events)
3216 unless $self->circ_test_success;
3219 my $runner = $self->script_runner;
3221 $runner->load($self->circ_permit_renew);
3222 my $result = $runner->run or
3223 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
3224 if ($result->{"events"}) {
3226 map { new OpenILS::Event($_) } @{$result->{"events"}}
3229 "circulator: circ_permit_renew for user " .
3230 $self->patron->id . " returned " .
3231 scalar(@{$result->{"events"}}) . " event(s)"
3235 $self->mk_script_runner;
3238 $logger->debug("circulator: re-creating script runner to be safe");
3242 # XXX: The primary mechanism for storing circ history is now handled
3243 # by tracking real circulation objects instead of bibs in a bucket.
3244 # However, this code is disabled by default and could be useful
3245 # some day, so may as well leave it for now.
3246 sub append_reading_list {
3250 $self->is_checkout and
3256 # verify history is globally enabled and uses the bucket mechanism
3257 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
3258 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
3260 return undef unless $htype and $htype eq 'bucket';
3262 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
3264 # verify the patron wants to retain the hisory
3265 my $setting = $e->search_actor_user_setting(
3266 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
3268 unless($setting and $setting->value) {
3273 my $bkt = $e->search_container_copy_bucket(
3274 {owner => $self->patron->id, btype => 'circ_history'})->[0];
3279 # find the next item position
3280 my $last_item = $e->search_container_copy_bucket_item(
3281 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
3282 $pos = $last_item->pos + 1 if $last_item;
3285 # create the history bucket if necessary
3286 $bkt = Fieldmapper::container::copy_bucket->new;
3287 $bkt->owner($self->patron->id);
3289 $bkt->btype('circ_history');
3291 $e->create_container_copy_bucket($bkt) or return $e->die_event;
3294 my $item = Fieldmapper::container::copy_bucket_item->new;
3296 $item->bucket($bkt->id);
3297 $item->target_copy($self->copy->id);
3300 $e->create_container_copy_bucket_item($item) or return $e->die_event;
3307 sub make_trigger_events {
3309 return unless $self->circ;
3310 $U->create_events_for_hook('checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
3311 $U->create_events_for_hook('checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
3312 $U->create_events_for_hook('renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
3317 sub checkin_handle_lost_now_found {
3318 my ($self, $bill_type) = @_;
3320 # ------------------------------------------------------------------
3321 # remove charge from patron's account if lost item is returned
3322 # ------------------------------------------------------------------
3324 my $bills = $self->editor->search_money_billing(
3326 xact => $self->circ->id,
3331 $logger->debug("voiding lost item charge of ".scalar(@$bills));
3332 for my $bill (@$bills) {
3333 if( !$U->is_true($bill->voided) ) {
3334 $logger->info("lost item returned - voiding bill ".$bill->id);
3336 $bill->void_time('now');
3337 $bill->voider($self->editor->requestor->id);
3338 my $note = ($bill->note) ? $bill->note . "\n" : '';
3339 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
3341 $self->bail_on_events($self->editor->event)
3342 unless $self->editor->update_money_billing($bill);
3347 sub checkin_handle_lost_now_found_restore_od {
3350 # ------------------------------------------------------------------
3351 # restore those overdue charges voided when item was set to lost
3352 # ------------------------------------------------------------------
3354 my $ods = $self->editor->search_money_billing(
3356 xact => $self->circ->id,
3361 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
3362 for my $bill (@$ods) {
3363 if( $U->is_true($bill->voided) ) {
3364 $logger->info("lost item returned - restoring overdue ".$bill->id);
3366 $bill->clear_void_time;
3367 $bill->voider($self->editor->requestor->id);
3368 my $note = ($bill->note) ? $bill->note . "\n" : '';
3369 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
3371 $self->bail_on_events($self->editor->event)
3372 unless $self->editor->update_money_billing($bill);