1 package OpenILS::Application::Circ::Circulate;
2 use strict; use warnings;
3 use base 'OpenILS::Application';
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw(:logger);
7 use OpenILS::Const qw/:const/;
8 use OpenILS::Application::AppUtils;
9 my $U = "OpenILS::Application::AppUtils";
13 my $legacy_script_support = 0;
18 my $conf = OpenSRF::Utils::SettingsClient->new;
19 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
21 $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
22 $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
24 my $lb = $conf->config_value( @pfx2, 'script_path' );
25 $lb = [ $lb ] unless ref($lb);
28 return unless $legacy_script_support;
30 my @pfx = ( @pfx2, "scripts" );
31 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
32 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
33 my $d = $conf->config_value( @pfx, 'circ_duration' );
34 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
35 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
36 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
38 $logger->error( "Missing circ script(s)" )
39 unless( $p and $c and $d and $f and $m and $pr );
41 $scripts{circ_permit_patron} = $p;
42 $scripts{circ_permit_copy} = $c;
43 $scripts{circ_duration} = $d;
44 $scripts{circ_recurring_fines}= $f;
45 $scripts{circ_max_fines} = $m;
46 $scripts{circ_permit_renew} = $pr;
49 "circulator: Loaded rules scripts for circ: " .
50 "circ permit patron = $p, ".
51 "circ permit copy = $c, ".
52 "circ duration = $d, ".
53 "circ recurring fines = $f, " .
54 "circ max fines = $m, ".
55 "circ renew permit = $pr. ".
57 "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
62 __PACKAGE__->register_method(
63 method => "run_method",
64 api_name => "open-ils.circ.checkout.permit",
66 Determines if the given checkout can occur
67 @param authtoken The login session key
68 @param params A trailing hash of named params including
69 barcode : The copy barcode,
70 patron : The patron the checkout is occurring for,
71 renew : true or false - whether or not this is a renewal
72 @return The event that occurred during the permit check.
76 __PACKAGE__->register_method (
77 method => 'run_method',
78 api_name => 'open-ils.circ.checkout.permit.override',
79 signature => q/@see open-ils.circ.checkout.permit/,
83 __PACKAGE__->register_method(
84 method => "run_method",
85 api_name => "open-ils.circ.checkout",
88 @param authtoken The login session key
89 @param params A named hash of params including:
91 barcode If no copy is provided, the copy is retrieved via barcode
92 copyid If no copy or barcode is provide, the copy id will be use
93 patron The patron's id
94 noncat True if this is a circulation for a non-cataloted item
95 noncat_type The non-cataloged type id
96 noncat_circ_lib The location for the noncat circ.
97 precat The item has yet to be cataloged
98 dummy_title The temporary title of the pre-cataloded item
99 dummy_author The temporary authr of the pre-cataloded item
100 Default is the home org of the staff member
101 @return The SUCCESS event on success, any other event depending on the error
104 __PACKAGE__->register_method(
105 method => "run_method",
106 api_name => "open-ils.circ.checkin",
109 Generic super-method for handling all copies
110 @param authtoken The login session key
111 @param params Hash of named parameters including:
112 barcode - The copy barcode
113 force - If true, copies in bad statuses will be checked in and give good statuses
114 noop - don't capture holds or put items into transit
115 void_overdues - void all overdues for the circulation (aka amnesty)
120 __PACKAGE__->register_method(
121 method => "run_method",
122 api_name => "open-ils.circ.checkin.override",
123 signature => q/@see open-ils.circ.checkin/
126 __PACKAGE__->register_method(
127 method => "run_method",
128 api_name => "open-ils.circ.renew.override",
129 signature => q/@see open-ils.circ.renew/,
133 __PACKAGE__->register_method(
134 method => "run_method",
135 api_name => "open-ils.circ.renew",
136 notes => <<" NOTES");
137 PARAMS( authtoken, circ => circ_id );
138 open-ils.circ.renew(login_session, circ_object);
139 Renews the provided circulation. login_session is the requestor of the
140 renewal and if the logged in user is not the same as circ->usr, then
141 the logged in user must have RENEW_CIRC permissions.
144 __PACKAGE__->register_method(
145 method => "run_method",
146 api_name => "open-ils.circ.checkout.full");
147 __PACKAGE__->register_method(
148 method => "run_method",
149 api_name => "open-ils.circ.checkout.full.override");
151 __PACKAGE__->register_method(
152 method => "run_method",
153 api_name => "open-ils.circ.checkout.inspect",
155 Returns the circ matrix test result and, on success, the rule set and matrix test object
162 my( $self, $conn, $auth, $args ) = @_;
163 translate_legacy_args($args);
164 my $api = $self->api_name;
167 OpenILS::Application::Circ::Circulator->new($auth, %$args);
169 return circ_events($circulator) if $circulator->bail_out;
171 # --------------------------------------------------------------------------
172 # Go ahead and load the script runner to make sure we have all
173 # of the objects we need
174 # --------------------------------------------------------------------------
175 $circulator->is_renewal(1) if $api =~ /renew/;
176 $circulator->is_checkin(1) if $api =~ /checkin/;
177 $circulator->noop if $circulator->claims_never_checked_out;
179 if($legacy_script_support and not $circulator->is_checkin) {
180 $circulator->mk_script_runner();
181 $circulator->legacy_script_support(1);
182 $circulator->circ_permit_patron($scripts{circ_permit_patron});
183 $circulator->circ_permit_copy($scripts{circ_permit_copy});
184 $circulator->circ_duration($scripts{circ_duration});
185 $circulator->circ_permit_renew($scripts{circ_permit_renew});
187 $circulator->mk_env();
189 return circ_events($circulator) if $circulator->bail_out;
192 $circulator->override(1) if $api =~ /override/o;
194 if( $api =~ /checkout\.permit/ ) {
195 $circulator->do_permit();
197 } elsif( $api =~ /checkout.full/ ) {
199 # requesting a precat checkout implies that any required
200 # overrides have been performed. Go ahead and re-override.
201 $circulator->override(1) if $circulator->request_precat;
202 $circulator->do_permit();
203 $circulator->is_checkout(1);
204 unless( $circulator->bail_out ) {
205 $circulator->events([]);
206 $circulator->do_checkout();
209 } elsif( $api =~ /inspect/ ) {
210 my $data = $circulator->do_inspect();
211 $circulator->editor->rollback;
214 } elsif( $api =~ /checkout/ ) {
215 $circulator->is_checkout(1);
216 $circulator->do_checkout();
218 } elsif( $api =~ /checkin/ ) {
219 $circulator->do_checkin();
221 } elsif( $api =~ /renew/ ) {
222 $circulator->is_renewal(1);
223 $circulator->do_renew();
226 if( $circulator->bail_out ) {
229 # make sure no success event accidentally slip in
231 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
234 my @e = @{$circulator->events};
235 push( @ee, $_->{textcode} ) for @e;
236 $logger->info("circulator: bailing out with events: @ee");
238 $circulator->editor->rollback;
241 $circulator->editor->commit;
244 $circulator->script_runner->cleanup if $circulator->script_runner;
246 $conn->respond_complete(circ_events($circulator));
248 unless($circulator->bail_out) {
249 $circulator->do_hold_notify($circulator->notify_hold)
250 if $circulator->notify_hold;
251 $circulator->retarget_holds if $circulator->retarget;
252 $circulator->append_reading_list;
253 $circulator->make_trigger_events;
259 my @e = @{$circ->events};
260 # if we have multiple events, SUCCESS should not be one of them;
261 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
262 return (@e == 1) ? $e[0] : \@e;
266 sub translate_legacy_args {
269 if( $$args{barcode} ) {
270 $$args{copy_barcode} = $$args{barcode};
271 delete $$args{barcode};
274 if( $$args{copyid} ) {
275 $$args{copy_id} = $$args{copyid};
276 delete $$args{copyid};
279 if( $$args{patronid} ) {
280 $$args{patron_id} = $$args{patronid};
281 delete $$args{patronid};
284 if( $$args{patron} and !ref($$args{patron}) ) {
285 $$args{patron_id} = $$args{patron};
286 delete $$args{patron};
290 if( $$args{noncat} ) {
291 $$args{is_noncat} = $$args{noncat};
292 delete $$args{noncat};
295 if( $$args{precat} ) {
296 $$args{is_precat} = $$args{request_precat} = $$args{precat};
297 delete $$args{precat};
303 # --------------------------------------------------------------------------
304 # This package actually manages all of the circulation logic
305 # --------------------------------------------------------------------------
306 package OpenILS::Application::Circ::Circulator;
307 use strict; use warnings;
308 use vars q/$AUTOLOAD/;
310 use OpenILS::Utils::Fieldmapper;
311 use OpenSRF::Utils::Cache;
312 use Digest::MD5 qw(md5_hex);
313 use DateTime::Format::ISO8601;
314 use OpenILS::Utils::PermitHold;
315 use OpenSRF::Utils qw/:datetime/;
316 use OpenSRF::Utils::SettingsClient;
317 use OpenILS::Application::Circ::Holds;
318 use OpenILS::Application::Circ::Transit;
319 use OpenSRF::Utils::Logger qw(:logger);
320 use OpenILS::Utils::CStoreEditor qw/:funcs/;
321 use OpenILS::Application::Circ::ScriptBuilder;
322 use OpenILS::Const qw/:const/;
323 use OpenILS::Utils::Penalty;
324 use OpenILS::Application::Circ::CircCommon;
327 my $holdcode = "OpenILS::Application::Circ::Holds";
328 my $transcode = "OpenILS::Application::Circ::Transit";
334 # --------------------------------------------------------------------------
335 # Add a pile of automagic getter/setter methods
336 # --------------------------------------------------------------------------
337 my @AUTOLOAD_FIELDS = qw/
381 recurring_fines_level
393 cancelled_hold_transit
399 circ_matrix_matchpoint
401 legacy_script_support
411 claims_never_checked_out
417 my $type = ref($self) or die "$self is not an object";
419 my $name = $AUTOLOAD;
422 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
423 $logger->error("circulator: $type: invalid autoload field: $name");
424 die "$type: invalid autoload field: $name\n"
429 *{"${type}::${name}"} = sub {
432 $s->{$name} = $v if defined $v;
436 return $self->$name($data);
441 my( $class, $auth, %args ) = @_;
442 $class = ref($class) || $class;
443 my $self = bless( {}, $class );
446 $self->editor(new_editor(xact => 1, authtoken => $auth));
448 unless( $self->editor->checkauth ) {
449 $self->bail_on_events($self->editor->event);
453 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
455 $self->$_($args{$_}) for keys %args;
458 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
460 # if this is a renewal, default to desk_renewal
461 $self->desk_renewal(1) unless
462 $self->opac_renewal or $self->phone_renewal;
464 $self->capture('') unless $self->capture;
466 unless(%user_groups) {
467 my $gps = $self->editor->retrieve_all_permission_grp_tree;
468 %user_groups = map { $_->id => $_ } @$gps;
475 # --------------------------------------------------------------------------
476 # True if we should discontinue processing
477 # --------------------------------------------------------------------------
479 my( $self, $bool ) = @_;
480 if( defined $bool ) {
481 $logger->info("circulator: BAILING OUT") if $bool;
482 $self->{bail_out} = $bool;
484 return $self->{bail_out};
489 my( $self, @evts ) = @_;
492 $logger->info("circulator: pushing event ".$e->{textcode});
493 push( @{$self->events}, $e ) unless
494 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
500 my $key = md5_hex( time() . rand() . "$$" );
501 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
502 return $self->permit_key($key);
505 sub check_permit_key {
507 my $key = $self->permit_key;
508 return 0 unless $key;
509 my $k = "oils_permit_key_$key";
510 my $one = $self->cache_handle->get_cache($k);
511 $self->cache_handle->delete_cache($k);
512 return ($one) ? 1 : 0;
517 my $e = $self->editor;
519 # --------------------------------------------------------------------------
520 # Grab the fleshed copy
521 # --------------------------------------------------------------------------
522 unless($self->is_noncat) {
526 flesh_fields => {acp => ['call_number'], acn => ['record']}
529 $copy = $e->retrieve_asset_copy(
530 [$self->copy_id, $flesh ]) or return $e->event;
532 } elsif( $self->copy_barcode ) {
534 $copy = $e->search_asset_copy(
535 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
540 $self->volume($copy->call_number);
541 $self->title($self->volume->record);
542 $self->copy->call_number($self->volume->id);
543 $self->volume->record($self->title->id);
544 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
545 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
546 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
547 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
550 # We can't renew if there is no copy
551 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
552 if $self->is_renewal;
557 # --------------------------------------------------------------------------
559 # --------------------------------------------------------------------------
561 if( $self->patron_id ) {
562 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
564 } elsif( $self->patron_barcode ) {
566 my $card = $e->search_actor_card(
567 {barcode => $self->patron_barcode})->[0] or return $e->event;
569 $patron = $e->search_actor_user(
570 {card => $card->id})->[0] or return $e->event;
573 if( my $copy = $self->copy ) {
574 my $circs = $e->search_action_circulation(
575 {target_copy => $copy->id, checkin_time => undef});
577 if( my $circ = $circs->[0] ) {
578 $patron = $e->retrieve_actor_user($circ->usr)
584 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
585 unless $self->patron($patron) or $self->is_checkin;
588 # --------------------------------------------------------------------------
589 # This builds the script runner environment and fetches most of the
591 # --------------------------------------------------------------------------
592 sub mk_script_runner {
598 qw/copy copy_barcode copy_id patron
599 patron_id patron_barcode volume title editor/;
601 # Translate our objects into the ScriptBuilder args hash
602 $$args{$_} = $self->$_() for @fields;
604 $args->{ignore_user_status} = 1 if $self->is_checkin;
605 $$args{fetch_patron_by_circ_copy} = 1;
606 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
608 if( my $pco = $self->pending_checkouts ) {
609 $logger->info("circulator: we were given a pending checkouts number of $pco");
610 $$args{patronItemsOut} = $pco;
613 # This fetches most of the objects we need
614 $self->script_runner(
615 OpenILS::Application::Circ::ScriptBuilder->build($args));
617 # Now we translate the ScriptBuilder objects back into self
618 $self->$_($$args{$_}) for @fields;
620 my @evts = @{$args->{_events}} if $args->{_events};
622 $logger->debug("circulator: script builder returned events: @evts") if @evts;
626 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
627 if(!$self->is_noncat and
629 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
633 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
634 return $self->bail_on_events(@e);
639 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
640 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
641 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
642 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
646 # We can't renew if there is no copy
647 return $self->bail_on_events(@evts) if
648 $self->is_renewal and !$self->copy;
650 # Set some circ-specific flags in the script environment
651 my $evt = "environment";
652 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
654 if( $self->is_noncat ) {
655 $self->script_runner->insert("$evt.isNonCat", 1);
656 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
659 if( $self->is_precat ) {
660 $self->script_runner->insert("environment.isPrecat", 1, 1);
663 $self->script_runner->add_path( $_ ) for @$script_libs;
668 # --------------------------------------------------------------------------
669 # Does the circ permit work
670 # --------------------------------------------------------------------------
674 $self->log_me("do_permit()");
676 unless( $self->editor->requestor->id == $self->patron->id ) {
677 return $self->bail_on_events($self->editor->event)
678 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
681 $self->check_captured_holds();
682 $self->do_copy_checks();
683 return if $self->bail_out;
684 $self->run_patron_permit_scripts();
685 $self->run_copy_permit_scripts()
686 unless $self->is_precat or $self->is_noncat;
687 $self->check_item_deposit_events();
688 $self->override_events();
689 return if $self->bail_out;
691 if($self->is_precat and not $self->request_precat) {
694 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
695 return $self->bail_out(1) unless $self->is_renewal;
699 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
702 sub check_item_deposit_events {
704 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
705 if $self->is_deposit and not $self->is_deposit_exempt;
706 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
707 if $self->is_rental and not $self->is_rental_exempt;
710 # returns true if the user is not required to pay deposits
711 sub is_deposit_exempt {
713 my $pid = (ref $self->patron->profile) ?
714 $self->patron->profile->id : $self->patron->profile;
715 my $groups = $U->ou_ancestor_setting_value(
716 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
717 for my $grp (@$groups) {
718 return 1 if $self->is_group_descendant($grp, $pid);
723 # returns true if the user is not required to pay rental fees
724 sub is_rental_exempt {
726 my $pid = (ref $self->patron->profile) ?
727 $self->patron->profile->id : $self->patron->profile;
728 my $groups = $U->ou_ancestor_setting_value(
729 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
730 for my $grp (@$groups) {
731 return 1 if $self->is_group_descendant($grp, $pid);
736 sub is_group_descendant {
737 my($self, $p_id, $c_id) = @_;
738 return 0 unless defined $p_id and defined $c_id;
739 return 1 if $c_id == $p_id;
740 while(my $grp = $user_groups{$c_id}) {
741 $c_id = $grp->parent;
742 return 0 unless defined $c_id;
743 return 1 if $c_id == $p_id;
748 sub check_captured_holds {
750 my $copy = $self->copy;
751 my $patron = $self->patron;
753 return undef unless $copy;
755 my $s = $U->copy_status($copy->status)->id;
756 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
757 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
759 # Item is on the holds shelf, make sure it's going to the right person
760 my $holds = $self->editor->search_action_hold_request(
763 current_copy => $copy->id ,
764 capture_time => { '!=' => undef },
765 cancel_time => undef,
766 fulfillment_time => undef
772 if( $holds and $$holds[0] ) {
773 return undef if $$holds[0]->usr == $patron->id;
776 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
778 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
784 my $copy = $self->copy;
787 my $stat = $U->copy_status($copy->status)->id;
789 # We cannot check out a copy if it is in-transit
790 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
791 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
794 $self->handle_claims_returned();
795 return if $self->bail_out;
797 # no claims returned circ was found, check if there is any open circ
798 unless( $self->is_renewal ) {
800 my $circs = $self->editor->search_action_circulation(
801 { target_copy => $copy->id, checkin_time => undef }
804 if(my $old_circ = $circs->[0]) { # an open circ was found
806 my $payload; # event payload
808 if($old_circ->usr == $self->patron->id) {
810 $payload = {old_circ => $old_circ};
812 # If there is an open circulation on the checkout item and an auto-renew
813 # interval is defined, inform the caller that they should go
814 # ahead and renew the item instead of warning about open circulations.
816 my $auto_renew_intvl = $U->ou_ancestor_setting_value(
817 $self->editor->requestor->ws_ou,
818 'circ.checkout_auto_renew_age',
822 if($auto_renew_intvl) {
823 my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
824 my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($old_circ->xact_start) );
826 if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
827 $payload->{auto_renew} = 1;
832 return $self->bail_on_events(
833 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
839 my $LEGACY_CIRC_EVENT_MAP = {
840 'actor.usr.barred' => 'PATRON_BARRED',
841 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
842 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
843 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
844 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
845 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
846 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
847 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
848 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
852 # ---------------------------------------------------------------------
853 # This pushes any patron-related events into the list but does not
854 # set bail_out for any events
855 # ---------------------------------------------------------------------
856 sub run_patron_permit_scripts {
858 my $runner = $self->script_runner;
859 my $patronid = $self->patron->id;
863 if(!$self->legacy_script_support) {
865 my $results = $self->run_indb_circ_test;
866 unless($self->circ_test_success) {
867 push(@allevents, OpenILS::Event->new(
868 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
874 # ---------------------------------------------------------------------
875 # # Now run the patron permit script
876 # ---------------------------------------------------------------------
877 $runner->load($self->circ_permit_patron);
878 my $result = $runner->run or
879 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
881 my $patron_events = $result->{events};
883 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
884 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
885 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
886 $penalties = $penalties->{fatal_penalties};
888 for my $pen (@$penalties) {
889 my $event = OpenILS::Event->new($pen->name);
890 $event->{desc} = $pen->label;
891 push(@allevents, $event);
894 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
897 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
899 $self->push_events(@allevents);
902 sub run_indb_circ_test {
904 return $self->matrix_test_result if $self->matrix_test_result;
906 my $dbfunc = ($self->is_renewal) ?
907 'action.item_user_renew_test' : 'action.item_user_circ_test';
909 my $results = $self->editor->json_query(
912 $self->editor->requestor->ws_ou,
913 ($self->is_precat or $self->is_noncat) ? undef : $self->copy->id,
919 $self->circ_test_success($U->is_true($results->[0]->{success}));
921 if(my $mp = $results->[0]->{matchpoint}) {
922 $self->circ_matrix_matchpoint(
923 $self->editor->retrieve_config_circ_matrix_matchpoint([
926 flesh_fields => {ccmm =>
927 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
933 return $self->matrix_test_result($results);
936 # ---------------------------------------------------------------------
937 # given a use and copy, this will calculate the circulation policy
938 # parameters. Only works with in-db circ.
939 # ---------------------------------------------------------------------
943 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
945 $self->run_indb_circ_test;
948 circ_test_success => $self->circ_test_success,
949 failure_events => [],
953 unless($self->circ_test_success) {
954 push(@{$results->{failure_codes}},
955 $_->{fail_part}) for @{$self->matrix_test_result};
956 push(@{$results->{failure_events}},
957 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part})
958 for @{$self->matrix_test_result};
961 if($self->circ_matrix_matchpoint) {
962 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
963 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
964 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
966 my $policy = $self->get_circ_policy(
967 $duration_rule, $recurring_fine_rule, $max_fine_rule);
969 $$results{$_} = $$policy{$_} for keys %$policy;
975 # ---------------------------------------------------------------------
976 # Loads the circ policy info for duration, recurring fine, and max
977 # fine based on the current copy
978 # ---------------------------------------------------------------------
979 sub get_circ_policy {
980 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
983 duration_rule => $duration_rule->name,
984 recurring_fine_rule => $recurring_fine_rule->name,
985 max_fine_rule => $max_fine_rule->name,
986 max_fine => $self->get_max_fine_amount($max_fine_rule),
987 fine_interval => $recurring_fine_rule->recurrance_interval,
988 renewal_remaining => $duration_rule->max_renewals
991 $policy->{duration} = $duration_rule->shrt
992 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
993 $policy->{duration} = $duration_rule->normal
994 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
995 $policy->{duration} = $duration_rule->extended
996 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
998 $policy->{recurring_fine} = $recurring_fine_rule->low
999 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1000 $policy->{recurring_fine} = $recurring_fine_rule->normal
1001 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1002 $policy->{recurring_fine} = $recurring_fine_rule->high
1003 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1008 sub get_max_fine_amount {
1010 my $max_fine_rule = shift;
1011 my $max_amount = $max_fine_rule->amount;
1013 # if is_percent is true then the max->amount is
1014 # use as a percentage of the copy price
1015 if ($U->is_true($max_fine_rule->is_percent)) {
1016 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1017 $max_amount = $price * $max_fine_rule->amount / 100;
1019 $U->ou_ancestor_setting_value(
1020 $self->circ->circ_lib,
1021 'circ.max_fine.cap_at_price',
1025 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1026 $max_amount = ( $price && $max_amount > $price ) ? $price : $max_amount;
1034 sub run_copy_permit_scripts {
1036 my $copy = $self->copy || return;
1037 my $runner = $self->script_runner;
1041 if(!$self->legacy_script_support) {
1042 my $results = $self->run_indb_circ_test;
1043 unless($self->circ_test_success) {
1044 push(@allevents, OpenILS::Event->new(
1045 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
1050 # ---------------------------------------------------------------------
1051 # Capture all of the copy permit events
1052 # ---------------------------------------------------------------------
1053 $runner->load($self->circ_permit_copy);
1054 my $result = $runner->run or
1055 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1056 my $copy_events = $result->{events};
1058 # ---------------------------------------------------------------------
1059 # Now collect all of the events together
1060 # ---------------------------------------------------------------------
1061 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1064 # See if this copy has an alert message
1065 my $ae = $self->check_copy_alert();
1066 push( @allevents, $ae ) if $ae;
1068 # uniquify the events
1069 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1070 @allevents = values %hash;
1073 $_->{payload} = $copy if
1074 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1077 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1079 $self->push_events(@allevents);
1083 sub check_copy_alert {
1085 return undef if $self->is_renewal;
1086 return OpenILS::Event->new(
1087 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1088 if $self->copy and $self->copy->alert_message;
1094 # --------------------------------------------------------------------------
1095 # If the call is overriding and has permissions to override every collected
1096 # event, the are cleared. Any event that the caller does not have
1097 # permission to override, will be left in the event list and bail_out will
1099 # XXX We need code in here to cancel any holds/transits on copies
1100 # that are being force-checked out
1101 # --------------------------------------------------------------------------
1102 sub override_events {
1104 my @events = @{$self->events};
1105 return unless @events;
1107 if(!$self->override) {
1108 return $self->bail_out(1)
1109 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1114 for my $e (@events) {
1115 my $tc = $e->{textcode};
1116 next if $tc eq 'SUCCESS';
1117 my $ov = "$tc.override";
1118 $logger->info("circulator: attempting to override event: $ov");
1120 return $self->bail_on_events($self->editor->event)
1121 unless( $self->editor->allowed($ov) );
1126 # --------------------------------------------------------------------------
1127 # If there is an open claimsreturn circ on the requested copy, close the
1128 # circ if overriding, otherwise bail out
1129 # --------------------------------------------------------------------------
1130 sub handle_claims_returned {
1132 my $copy = $self->copy;
1134 my $CR = $self->editor->search_action_circulation(
1136 target_copy => $copy->id,
1137 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1138 checkin_time => undef,
1142 return unless ($CR = $CR->[0]);
1146 # - If the caller has set the override flag, we will check the item in
1147 if($self->override) {
1149 $CR->checkin_time('now');
1150 $CR->checkin_scan_time('now');
1151 $CR->checkin_lib($self->editor->requestor->ws_ou);
1152 $CR->checkin_workstation($self->editor->requestor->wsid);
1153 $CR->checkin_staff($self->editor->requestor->id);
1155 $evt = $self->editor->event
1156 unless $self->editor->update_action_circulation($CR);
1159 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1162 $self->bail_on_events($evt) if $evt;
1167 # --------------------------------------------------------------------------
1168 # This performs the checkout
1169 # --------------------------------------------------------------------------
1173 $self->log_me("do_checkout()");
1175 # make sure perms are good if this isn't a renewal
1176 unless( $self->is_renewal ) {
1177 return $self->bail_on_events($self->editor->event)
1178 unless( $self->editor->allowed('COPY_CHECKOUT') );
1181 # verify the permit key
1182 unless( $self->check_permit_key ) {
1183 if( $self->permit_override ) {
1184 return $self->bail_on_events($self->editor->event)
1185 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1187 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1191 # if this is a non-cataloged circ, build the circ and finish
1192 if( $self->is_noncat ) {
1193 $self->checkout_noncat;
1195 OpenILS::Event->new('SUCCESS',
1196 payload => { noncat_circ => $self->circ }));
1200 if( $self->is_precat ) {
1201 $self->make_precat_copy;
1202 return if $self->bail_out;
1204 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1205 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1208 $self->do_copy_checks;
1209 return if $self->bail_out;
1211 $self->run_checkout_scripts();
1212 return if $self->bail_out;
1214 $self->build_checkout_circ_object();
1215 return if $self->bail_out;
1217 $self->apply_modified_due_date();
1218 return if $self->bail_out;
1220 return $self->bail_on_events($self->editor->event)
1221 unless $self->editor->create_action_circulation($self->circ);
1223 # refresh the circ to force local time zone for now
1224 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1226 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1228 return if $self->bail_out;
1230 $self->apply_deposit_fee();
1231 return if $self->bail_out;
1233 $self->handle_checkout_holds();
1234 return if $self->bail_out;
1236 # ------------------------------------------------------------------------------
1237 # Update the patron penalty info in the DB. Run it for permit-overrides
1238 # since the penalties are not updated during the permit phase
1239 # ------------------------------------------------------------------------------
1240 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1242 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1245 if($self->is_renewal) {
1246 # flesh the billing summary for the checked-in circ
1247 $pcirc = $self->editor->retrieve_action_circulation([
1249 {flesh => 2, flesh_fields => {circ => ['billable_transaction'], mbt => ['summary']}}
1254 OpenILS::Event->new('SUCCESS',
1256 copy => $U->unflesh_copy($self->copy),
1257 circ => $self->circ,
1259 holds_fulfilled => $self->fulfilled_holds,
1260 deposit_billing => $self->deposit_billing,
1261 rental_billing => $self->rental_billing,
1262 parent_circ => $pcirc,
1263 patron => ($self->return_patron) ? $self->patron : undef
1269 sub apply_deposit_fee {
1271 my $copy = $self->copy;
1273 ($self->is_deposit and not $self->is_deposit_exempt) or
1274 ($self->is_rental and not $self->is_rental_exempt);
1276 my $bill = Fieldmapper::money::billing->new;
1277 my $amount = $copy->deposit_amount;
1281 if($self->is_deposit) {
1282 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1284 $self->deposit_billing($bill);
1286 $billing_type = OILS_BILLING_TYPE_RENTAL;
1288 $self->rental_billing($bill);
1291 $bill->xact($self->circ->id);
1292 $bill->amount($amount);
1293 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1294 $bill->billing_type($billing_type);
1295 $bill->btype($btype);
1296 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1298 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1303 my $copy = $self->copy;
1305 my $stat = $copy->status if ref $copy->status;
1306 my $loc = $copy->location if ref $copy->location;
1307 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1309 $copy->status($stat->id) if $stat;
1310 $copy->location($loc->id) if $loc;
1311 $copy->circ_lib($circ_lib->id) if $circ_lib;
1312 $copy->editor($self->editor->requestor->id);
1313 $copy->edit_date('now');
1314 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1316 return $self->bail_on_events($self->editor->event)
1317 unless $self->editor->update_asset_copy($self->copy);
1319 $copy->status($U->copy_status($copy->status));
1320 $copy->location($loc) if $loc;
1321 $copy->circ_lib($circ_lib) if $circ_lib;
1325 sub bail_on_events {
1326 my( $self, @evts ) = @_;
1327 $self->push_events(@evts);
1332 # ------------------------------------------------------------------------------
1333 # When an item is checked out, see if we can fulfill a hold for this patron
1334 # ------------------------------------------------------------------------------
1335 sub handle_checkout_holds {
1337 my $copy = $self->copy;
1338 my $patron = $self->patron;
1340 my $e = $self->editor;
1341 $self->fulfilled_holds([]);
1343 # pre/non-cats can't fulfill a hold
1344 return if $self->is_precat or $self->is_noncat;
1346 my $hold = $e->search_action_hold_request({
1347 current_copy => $copy->id ,
1348 cancel_time => undef,
1349 fulfillment_time => undef,
1351 {expire_time => undef},
1352 {expire_time => {'>' => 'now'}}
1356 if($hold and $hold->usr != $patron->id) {
1357 # reset the hold since the copy is now checked out
1359 $logger->info("circulator: un-targeting hold ".$hold->id.
1360 " because copy ".$copy->id." is getting checked out");
1362 $hold->clear_prev_check_time;
1363 $hold->clear_current_copy;
1364 $hold->clear_capture_time;
1366 return $self->bail_on_event($e->event)
1367 unless $e->update_action_hold_request($hold);
1373 $hold = $self->find_related_user_hold($copy, $patron) or return;
1374 $logger->info("circulator: found related hold to fulfill in checkout");
1377 $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1379 # if the hold was never officially captured, capture it.
1380 $hold->current_copy($copy->id);
1381 $hold->capture_time('now') unless $hold->capture_time;
1382 $hold->fulfillment_time('now');
1383 $hold->fulfillment_staff($e->requestor->id);
1384 $hold->fulfillment_lib($e->requestor->ws_ou);
1386 return $self->bail_on_events($e->event)
1387 unless $e->update_action_hold_request($hold);
1389 $holdcode->delete_hold_copy_maps($e, $hold->id);
1390 return $self->fulfilled_holds([$hold->id]);
1394 # ------------------------------------------------------------------------------
1395 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1396 # the patron directly targets the checked out item, see if there is another hold
1397 # (with hold_type T or V) for the patron that could be fulfilled by the checked
1398 # out item. Fulfill the oldest hold and only fulfill 1 of them.
1399 # ------------------------------------------------------------------------------
1400 sub find_related_user_hold {
1401 my($self, $copy, $patron) = @_;
1402 my $e = $self->editor;
1404 return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
1406 return undef unless $U->ou_ancestor_setting_value(
1407 $e->requestor->ws_ou, 'circ.checkout_fills_related_hold', $e);
1409 # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1411 select => {ahr => ['id']},
1416 fkey => 'current_copy',
1417 type => 'left' # there may be no current_copy
1424 fulfillment_time => undef,
1425 cancel_time => undef,
1427 {expire_time => undef},
1428 {expire_time => {'>' => 'now'}}
1435 target => $self->volume->id
1441 target => $self->title->id
1447 {id => undef}, # left-join copy may be nonexistent
1448 {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1452 order_by => {ahr => {request_time => {direction => 'asc'}}},
1456 my $hold_info = $e->json_query($args)->[0];
1457 return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1462 sub run_checkout_scripts {
1466 my $runner = $self->script_runner;
1475 if(!$self->legacy_script_support) {
1476 $self->run_indb_circ_test();
1477 $duration = $self->circ_matrix_matchpoint->duration_rule;
1478 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1479 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1483 $runner->load($self->circ_duration);
1485 my $result = $runner->run or
1486 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1488 $duration_name = $result->{durationRule};
1489 $recurring_name = $result->{recurringFinesRule};
1490 $max_fine_name = $result->{maxFine};
1493 $duration_name = $duration->name if $duration;
1494 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1497 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1498 return $self->bail_on_events($evt) if $evt;
1500 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1501 return $self->bail_on_events($evt) if $evt;
1503 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1504 return $self->bail_on_events($evt) if $evt;
1509 # The item circulates with an unlimited duration
1515 $self->duration_rule($duration);
1516 $self->recurring_fines_rule($recurring);
1517 $self->max_fine_rule($max_fine);
1521 sub build_checkout_circ_object {
1524 my $circ = Fieldmapper::action::circulation->new;
1525 my $duration = $self->duration_rule;
1526 my $max = $self->max_fine_rule;
1527 my $recurring = $self->recurring_fines_rule;
1528 my $copy = $self->copy;
1529 my $patron = $self->patron;
1533 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1535 my $dname = $duration->name;
1536 my $mname = $max->name;
1537 my $rname = $recurring->name;
1539 $logger->debug("circulator: building circulation ".
1540 "with duration=$dname, maxfine=$mname, recurring=$rname");
1542 $circ->duration($policy->{duration});
1543 $circ->recurring_fine($policy->{recurring_fine});
1544 $circ->duration_rule($duration->name);
1545 $circ->recurring_fine_rule($recurring->name);
1546 $circ->max_fine_rule($max->name);
1547 $circ->max_fine($policy->{max_fine});
1548 $circ->fine_interval($recurring->recurrance_interval);
1549 $circ->renewal_remaining($duration->max_renewals);
1553 $logger->info("circulator: copy found with an unlimited circ duration");
1554 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1555 $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1556 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1557 $circ->renewal_remaining(0);
1560 $circ->target_copy( $copy->id );
1561 $circ->usr( $patron->id );
1562 $circ->circ_lib( $self->circ_lib );
1563 $circ->workstation($self->editor->requestor->wsid)
1564 if defined $self->editor->requestor->wsid;
1566 # renewals maintain a link to the parent circulation
1567 $circ->parent_circ($self->parent_circ);
1569 if( $self->is_renewal ) {
1570 $circ->opac_renewal('t') if $self->opac_renewal;
1571 $circ->phone_renewal('t') if $self->phone_renewal;
1572 $circ->desk_renewal('t') if $self->desk_renewal;
1573 $circ->renewal_remaining($self->renewal_remaining);
1574 $circ->circ_staff($self->editor->requestor->id);
1578 # if the user provided an overiding checkout time,
1579 # (e.g. the checkout really happened several hours ago), then
1580 # we apply that here. Does this need a perm??
1581 $circ->xact_start(clense_ISO8601($self->checkout_time))
1582 if $self->checkout_time;
1584 # if a patron is renewing, 'requestor' will be the patron
1585 $circ->circ_staff($self->editor->requestor->id);
1586 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1592 sub apply_modified_due_date {
1594 my $circ = $self->circ;
1595 my $copy = $self->copy;
1597 if( $self->due_date ) {
1599 return $self->bail_on_events($self->editor->event)
1600 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1602 $circ->due_date(clense_ISO8601($self->due_date));
1606 # if the due_date lands on a day when the location is closed
1607 return unless $copy and $circ->due_date;
1609 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1611 # due-date overlap should be determined by the location the item
1612 # is checked out from, not the owning or circ lib of the item
1613 my $org = $self->editor->requestor->ws_ou;
1615 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1616 " with an item due date of ".$circ->due_date );
1618 my $dateinfo = $U->storagereq(
1619 'open-ils.storage.actor.org_unit.closed_date.overlap',
1620 $org, $circ->due_date );
1623 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1624 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1626 # XXX make the behavior more dynamic
1627 # for now, we just push the due date to after the close date
1628 $circ->due_date($dateinfo->{end});
1635 sub create_due_date {
1636 my( $self, $duration ) = @_;
1638 # if there is a raw time component (e.g. from postgres),
1639 # turn it into an interval that interval_to_seconds can parse
1640 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1642 # for now, use the server timezone. TODO: use workstation org timezone
1643 my $due_date = DateTime->now(time_zone => 'local');
1645 # add the circ duration
1646 $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
1648 # return ISO8601 time with timezone
1649 return $due_date->strftime('%FT%T%z');
1654 sub make_precat_copy {
1656 my $copy = $self->copy;
1659 $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1661 $copy->editor($self->editor->requestor->id);
1662 $copy->edit_date('now');
1663 $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
1664 $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
1665 $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
1666 $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
1667 $self->update_copy();
1671 $logger->info("circulator: Creating a new precataloged ".
1672 "copy in checkout with barcode " . $self->copy_barcode);
1674 $copy = Fieldmapper::asset::copy->new;
1675 $copy->circ_lib($self->circ_lib);
1676 $copy->creator($self->editor->requestor->id);
1677 $copy->editor($self->editor->requestor->id);
1678 $copy->barcode($self->copy_barcode);
1679 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1680 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1681 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1683 $copy->dummy_title($self->dummy_title || "");
1684 $copy->dummy_author($self->dummy_author || "");
1685 $copy->dummy_isbn($self->dummy_isbn || "");
1686 $copy->circ_modifier($self->circ_modifier);
1689 # See if we need to override the circ_lib for the copy with a configured circ_lib
1690 # Setting is shortname of the org unit
1691 my $precat_circ_lib = $U->ou_ancestor_setting_value(
1692 $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
1694 if($precat_circ_lib) {
1695 my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
1698 $self->bail_on_events($self->editor->event);
1702 $copy->circ_lib($org->id);
1706 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1708 $self->push_events($self->editor->event);
1712 # this is a little bit of a hack, but we need to
1713 # get the copy into the script runner
1714 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1718 sub checkout_noncat {
1724 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1725 my $count = $self->noncat_count || 1;
1726 my $cotime = clense_ISO8601($self->checkout_time) || "";
1728 $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
1732 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1733 $self->editor->requestor->id,
1741 $self->push_events($evt);
1752 $self->log_me("do_checkin()");
1754 return $self->bail_on_events(
1755 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1758 if( $self->checkin_check_holds_shelf() ) {
1759 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1760 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1761 $self->checkin_flesh_events;
1765 unless( $self->is_renewal ) {
1766 return $self->bail_on_events($self->editor->event)
1767 unless $self->editor->allowed('COPY_CHECKIN');
1770 $self->push_events($self->check_copy_alert());
1771 $self->push_events($self->check_checkin_copy_status());
1773 # the renew code will have already found our circulation object
1774 unless( $self->is_renewal and $self->circ ) {
1775 my $circs = $self->editor->search_action_circulation(
1776 { target_copy => $self->copy->id, checkin_time => undef });
1777 $self->circ($$circs[0]);
1779 # for now, just warn if there are multiple open circs on a copy
1780 $logger->warn("circulator: we have ".scalar(@$circs).
1781 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1784 # run the fine generator against this circ, if this circ is there
1785 $self->generate_fines if ($self->circ);
1787 # if the circ is marked as 'claims returned', add the event to the list
1788 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1789 if ($self->circ and $self->circ->stop_fines
1790 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1792 $self->check_circ_deposit();
1794 # handle the overridable events
1795 $self->override_events unless $self->is_renewal;
1796 return if $self->bail_out;
1800 $self->editor->search_action_transit_copy(
1801 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1805 $self->checkin_handle_circ;
1806 return if $self->bail_out;
1807 $self->checkin_changed(1);
1809 } elsif( $self->transit ) {
1810 my $hold_transit = $self->process_received_transit;
1811 $self->checkin_changed(1);
1813 if( $self->bail_out ) {
1814 $self->checkin_flesh_events;
1818 if( my $e = $self->check_checkin_copy_status() ) {
1819 # If the original copy status is special, alert the caller
1820 my $ev = $self->events;
1821 $self->events([$e]);
1822 $self->override_events;
1823 return if $self->bail_out;
1827 if( $hold_transit or
1828 $U->copy_status($self->copy->status)->id
1829 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1832 if( $hold_transit ) {
1833 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1835 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1840 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1842 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1843 $self->reshelve_copy(1);
1844 $self->cancelled_hold_transit(1);
1845 $self->notify_hold(0); # don't notify for cancelled holds
1846 return if $self->bail_out;
1850 # hold transited to correct location
1851 $self->checkin_flesh_events;
1856 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1858 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1859 " that is in-transit, but there is no transit.. repairing");
1860 $self->reshelve_copy(1);
1861 return if $self->bail_out;
1864 if( $self->is_renewal ) {
1865 $self->push_events(OpenILS::Event->new('SUCCESS'));
1869 # ------------------------------------------------------------------------------
1870 # Circulations and transits are now closed where necessary. Now go on to see if
1871 # this copy can fulfill a hold or needs to be routed to a different location
1872 # ------------------------------------------------------------------------------
1874 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1876 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1877 return if $self->bail_out;
1879 unless($needed_for_hold) {
1880 my $circ_lib = (ref $self->copy->circ_lib) ?
1881 $self->copy->circ_lib->id : $self->copy->circ_lib;
1883 if( $self->remote_hold ) {
1884 $circ_lib = $self->remote_hold->pickup_lib;
1885 $logger->warn("circulator: Copy ".$self->copy->barcode.
1886 " is on a remote hold's shelf, sending to $circ_lib");
1889 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1891 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1893 $self->checkin_handle_precat();
1894 return if $self->bail_out;
1898 my $bc = $self->copy->barcode;
1899 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1900 $self->checkin_build_copy_transit($circ_lib);
1901 return if $self->bail_out;
1902 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1907 if($self->claims_never_checked_out and
1908 $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
1910 # the item was not supposed to be checked out to the user and should now be marked as missing
1911 $self->copy->status(OILS_COPY_STATUS_MISSING);
1915 $self->reshelve_copy;
1918 return if $self->bail_out;
1920 unless($self->checkin_changed) {
1922 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1923 my $stat = $U->copy_status($self->copy->status)->id;
1925 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1926 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1927 $self->bail_out(1); # no need to commit anything
1931 $self->push_events(OpenILS::Event->new('SUCCESS'))
1932 unless @{$self->events};
1935 OpenILS::Utils::Penalty->calculate_penalties(
1936 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1938 $self->checkin_flesh_events;
1942 # if a deposit was payed for this item, push the event
1943 sub check_circ_deposit {
1945 return unless $self->circ;
1946 my $deposit = $self->editor->search_money_billing(
1948 xact => $self->circ->id,
1950 }, {idlist => 1})->[0];
1952 $self->push_events(OpenILS::Event->new(
1953 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1958 my $force = $self->force || shift;
1959 my $copy = $self->copy;
1961 my $stat = $U->copy_status($copy->status)->id;
1964 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1965 $stat != OILS_COPY_STATUS_CATALOGING and
1966 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1967 $stat != OILS_COPY_STATUS_RESHELVING )) {
1969 $copy->status( OILS_COPY_STATUS_RESHELVING );
1971 $self->checkin_changed(1);
1976 # Returns true if the item is at the current location
1977 # because it was transited there for a hold and the
1978 # hold has not been fulfilled
1979 sub checkin_check_holds_shelf {
1981 return 0 unless $self->copy;
1984 $U->copy_status($self->copy->status)->id ==
1985 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1987 # find the hold that put us on the holds shelf
1988 my $holds = $self->editor->search_action_hold_request(
1990 current_copy => $self->copy->id,
1991 capture_time => { '!=' => undef },
1992 fulfillment_time => undef,
1993 cancel_time => undef,
1998 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1999 $self->reshelve_copy(1);
2003 my $hold = $$holds[0];
2005 $logger->info("circulator: we found a captured, un-fulfilled hold [".
2006 $hold->id. "] for copy ".$self->copy->barcode);
2008 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2009 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
2013 $logger->info("circulator: hold is not for here..");
2014 $self->remote_hold($hold);
2019 sub checkin_handle_precat {
2021 my $copy = $self->copy;
2023 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
2024 $copy->status(OILS_COPY_STATUS_CATALOGING);
2025 $self->update_copy();
2026 $self->checkin_changed(1);
2027 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
2032 sub checkin_build_copy_transit {
2035 my $copy = $self->copy;
2036 my $transit = Fieldmapper::action::transit_copy->new;
2038 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
2039 $logger->info("circulator: transiting copy to $dest");
2041 $transit->source($self->editor->requestor->ws_ou);
2042 $transit->dest($dest);
2043 $transit->target_copy($copy->id);
2044 $transit->source_send_time('now');
2045 $transit->copy_status( $U->copy_status($copy->status)->id );
2047 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2049 return $self->bail_on_events($self->editor->event)
2050 unless $self->editor->create_action_transit_copy($transit);
2052 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2054 $self->checkin_changed(1);
2058 # returns true if the item was used (or may potentially be used
2059 # in subsequent calls) to capture a hold.
2060 sub attempt_checkin_hold_capture {
2062 my $copy = $self->copy;
2064 # we've been explicitly told not to capture any holds
2065 return 0 if $self->capture eq 'nocapture';
2067 # See if this copy can fulfill any holds
2068 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
2069 $self->editor, $copy, $self->editor->requestor );
2072 $logger->debug("circulator: no potential permitted".
2073 "holds found for copy ".$copy->barcode);
2077 if($self->capture ne 'capture') {
2078 # see if this item is in a hold-capture-delay location
2079 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2080 if($U->is_true($location->hold_verify)) {
2081 $self->bail_on_events(
2082 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2087 $self->retarget($retarget);
2089 $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2091 $hold->current_copy($copy->id);
2092 $hold->capture_time('now');
2093 $self->put_hold_on_shelf($hold)
2094 if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2096 # prevent DB errors caused by fetching
2097 # holds from storage, and updating through cstore
2098 $hold->clear_fulfillment_time;
2099 $hold->clear_fulfillment_staff;
2100 $hold->clear_fulfillment_lib;
2101 $hold->clear_expire_time;
2102 $hold->clear_cancel_time;
2103 $hold->clear_prev_check_time unless $hold->prev_check_time;
2105 $self->bail_on_events($self->editor->event)
2106 unless $self->editor->update_action_hold_request($hold);
2108 $self->checkin_changed(1);
2110 return 0 if $self->bail_out;
2112 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2114 # This hold was captured in the correct location
2115 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2116 $self->push_events(OpenILS::Event->new('SUCCESS'));
2118 #$self->do_hold_notify($hold->id);
2119 $self->notify_hold($hold->id);
2123 # Hold needs to be picked up elsewhere. Build a hold
2124 # transit and route the item.
2125 $self->checkin_build_hold_transit();
2126 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2127 return 0 if $self->bail_out;
2128 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2131 # make sure we save the copy status
2136 sub do_hold_notify {
2137 my( $self, $holdid ) = @_;
2139 my $e = new_editor(xact => 1);
2140 my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2142 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2143 $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2145 $logger->info("circulator: running delayed hold notify process");
2147 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2148 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2150 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2151 hold_id => $holdid, requestor => $self->editor->requestor);
2153 $logger->debug("circulator: built hold notifier");
2155 if(!$notifier->event) {
2157 $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2159 my $stat = $notifier->send_email_notify;
2160 if( $stat == '1' ) {
2161 $logger->info("circulator: hold notify succeeded for hold $holdid");
2165 $logger->warn("circulator: * hold notify failed for hold $holdid");
2168 $logger->info("circulator: Not sending hold notification since the patron has no email address");
2172 sub retarget_holds {
2174 $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2175 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2176 $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2177 # no reason to wait for the return value
2181 sub checkin_build_hold_transit {
2184 my $copy = $self->copy;
2185 my $hold = $self->hold;
2186 my $trans = Fieldmapper::action::hold_transit_copy->new;
2188 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2190 $trans->hold($hold->id);
2191 $trans->source($self->editor->requestor->ws_ou);
2192 $trans->dest($hold->pickup_lib);
2193 $trans->source_send_time("now");
2194 $trans->target_copy($copy->id);
2196 # when the copy gets to its destination, it will recover
2197 # this status - put it onto the holds shelf
2198 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2200 return $self->bail_on_events($self->editor->event)
2201 unless $self->editor->create_action_hold_transit_copy($trans);
2206 sub process_received_transit {
2208 my $copy = $self->copy;
2209 my $copyid = $self->copy->id;
2211 my $status_name = $U->copy_status($copy->status)->name;
2212 $logger->debug("circulator: attempting transit receive on ".
2213 "copy $copyid. Copy status is $status_name");
2215 my $transit = $self->transit;
2217 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2218 # - this item is in-transit to a different location
2220 my $tid = $transit->id;
2221 my $loc = $self->editor->requestor->ws_ou;
2222 my $dest = $transit->dest;
2224 $logger->info("circulator: Fowarding transit on copy which is destined ".
2225 "for a different location. transit=$tid, copy=$copyid, current ".
2226 "location=$loc, destination location=$dest");
2228 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2230 # grab the associated hold object if available
2231 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2232 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2234 return $self->bail_on_events($evt);
2237 # The transit is received, set the receive time
2238 $transit->dest_recv_time('now');
2239 $self->bail_on_events($self->editor->event)
2240 unless $self->editor->update_action_transit_copy($transit);
2242 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2244 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2245 $copy->status( $transit->copy_status );
2246 $self->update_copy();
2247 return if $self->bail_out;
2251 my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2253 # hold has arrived at destination, set shelf time
2254 $self->put_hold_on_shelf($hold);
2255 $self->bail_on_events($self->editor->event)
2256 unless $self->editor->update_action_hold_request($hold);
2257 return if $self->bail_out;
2259 $self->notify_hold($hold_transit->hold);
2264 OpenILS::Event->new(
2267 payload => { transit => $transit, holdtransit => $hold_transit } ));
2269 return $hold_transit;
2273 # ------------------------------------------------------------------
2274 # Sets the shelf_time and shelf_expire_time for a newly shelved hold
2275 # ------------------------------------------------------------------
2276 sub put_hold_on_shelf {
2277 my($self, $hold) = @_;
2279 $hold->shelf_time('now');
2281 my $shelf_expire = $U->ou_ancestor_setting_value(
2282 $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
2285 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
2286 my $expire_time = DateTime->now->add(seconds => $seconds);
2287 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
2295 sub generate_fines {
2300 my $st = OpenSRF::AppSession->connect('open-ils.storage');
2303 'open-ils.storage.action.circulation.overdue.generate_fines',
2310 # refresh the circ in case the fine generator set the stop_fines field
2311 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
2316 sub checkin_handle_circ {
2318 my $circ = $self->circ;
2319 my $copy = $self->copy;
2323 $self->backdate($circ->xact_start) if $self->claims_never_checked_out;
2325 # backdate the circ if necessary
2326 if($self->backdate) {
2327 $self->checkin_handle_backdate;
2328 return if $self->bail_out;
2331 if($self->void_overdues) {
2332 my $evt = OpenILS::Application::Circ::CircCommon->void_overdues(
2333 $self->editor, $circ, undef, 'System: Amnesty Checkin'); # TODO i18n for system-generated notes
2334 return $self->bail_on_events($evt) if $evt;
2337 if(!$circ->stop_fines) {
2338 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2339 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2340 $circ->stop_fines(OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT) if $self->claims_never_checked_out;
2341 $circ->stop_fines_time('now');
2342 $circ->stop_fines_time($self->backdate) if $self->backdate;
2345 # see if there are any fines owed on this circ. if not, close it
2346 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2347 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2349 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2351 # Set the checkin vars since we have the item
2352 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2354 # capture the true scan time for back-dated checkins
2355 $circ->checkin_scan_time('now');
2357 $circ->checkin_staff($self->editor->requestor->id);
2358 $circ->checkin_lib($self->editor->requestor->ws_ou);
2359 $circ->checkin_workstation($self->editor->requestor->wsid);
2361 my $circ_lib = (ref $self->copy->circ_lib) ?
2362 $self->copy->circ_lib->id : $self->copy->circ_lib;
2363 my $stat = $U->copy_status($self->copy->status)->id;
2365 # immediately available keeps items lost or missing items from going home before being handled
2366 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2367 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2370 if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2372 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2373 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2375 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2379 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2381 $self->checkin_handle_lost($circ_lib);
2385 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2389 return $self->bail_on_events($self->editor->event)
2390 unless $self->editor->update_action_circulation($circ);
2392 # make sure the circ isn't closed if we just voided some fines
2393 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2394 return $self->bail_on_events($evt) if $evt;
2400 # ------------------------------------------------------------------
2401 # See if we need to void billings for lost checkin
2402 # ------------------------------------------------------------------
2403 sub checkin_handle_lost {
2405 my $circ_lib = shift;
2406 my $circ = $self->circ;
2408 my $max_return = $U->ou_ancestor_setting_value(
2409 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2414 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2415 $tm[5] -= 1 if $tm[5] > 0;
2416 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2418 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2419 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2421 $max_return = 0 if $today < $last_chance;
2424 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2426 my $void_lost = $U->ou_ancestor_setting_value(
2427 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2428 my $void_lost_fee = $U->ou_ancestor_setting_value(
2429 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2430 my $restore_od = $U->ou_ancestor_setting_value(
2431 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2433 $self->checkin_handle_lost_now_found(3) if $void_lost;
2434 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2435 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2438 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2443 sub checkin_handle_backdate {
2446 my $bd = $self->backdate;
2448 # ------------------------------------------------------------------
2449 # clean up the backdate for date comparison
2450 # we want any bills created on or after the backdate
2451 # ------------------------------------------------------------------
2452 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2453 #$bd = "${bd}T23:59:59";
2455 my $bills = $self->editor->search_money_billing(
2457 billing_ts => { '>=' => $bd },
2458 xact => $self->circ->id,
2463 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2465 for my $bill (@$bills) {
2466 unless( $U->is_true($bill->voided) ) {
2467 $logger->info("backdate voiding bill ".$bill->id);
2469 $bill->void_time('now');
2470 $bill->voider($self->editor->requestor->id);
2471 my $n = $bill->note || "";
2472 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2474 $self->bail_on_events($self->editor->event)
2475 unless $self->editor->update_money_billing($bill);
2483 sub find_patron_from_copy {
2485 my $circs = $self->editor->search_action_circulation(
2486 { target_copy => $self->copy->id, checkin_time => undef });
2487 my $circ = $circs->[0];
2488 return unless $circ;
2489 my $u = $self->editor->retrieve_actor_user($circ->usr)
2490 or return $self->bail_on_events($self->editor->event);
2494 sub check_checkin_copy_status {
2496 my $copy = $self->copy;
2502 my $status = $U->copy_status($copy->status)->id;
2505 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2506 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2507 $status == OILS_COPY_STATUS_IN_PROCESS ||
2508 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2509 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2510 $status == OILS_COPY_STATUS_CATALOGING ||
2511 $status == OILS_COPY_STATUS_RESHELVING );
2513 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2514 if( $status == OILS_COPY_STATUS_LOST );
2516 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2517 if( $status == OILS_COPY_STATUS_MISSING );
2519 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2524 # --------------------------------------------------------------------------
2525 # On checkin, we need to return as many relevant objects as we can
2526 # --------------------------------------------------------------------------
2527 sub checkin_flesh_events {
2530 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2531 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2532 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2535 my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2538 if($self->hold and !$self->hold->cancel_time) {
2539 $hold = $self->hold;
2540 $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
2544 # if we checked in a circulation, flesh the billing summary data
2545 $self->circ->billable_transaction(
2546 $self->editor->retrieve_money_billable_transaction([
2548 {flesh => 1, flesh_fields => {mbt => ['summary']}}
2554 # flesh some patron fields before returning
2556 $self->editor->retrieve_actor_user([
2561 au => ['card', 'billing_address', 'mailing_address']
2568 for my $evt (@{$self->events}) {
2571 $payload->{copy} = $U->unflesh_copy($self->copy);
2572 $payload->{record} = $record,
2573 $payload->{circ} = $self->circ;
2574 $payload->{transit} = $self->transit;
2575 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2576 $payload->{hold} = $hold;
2577 $payload->{patron} = $self->patron;
2578 $evt->{payload} = $payload;
2583 my( $self, $msg ) = @_;
2584 my $bc = ($self->copy) ? $self->copy->barcode :
2587 my $usr = ($self->patron) ? $self->patron->id : "";
2588 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2589 ", recipient=$usr, copy=$bc");
2595 $self->log_me("do_renew()");
2597 # Make sure there is an open circ to renew that is not
2598 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2599 my $usrid = $self->patron->id if $self->patron;
2602 # If we have a patron, match them to the circ
2603 $circ = $self->editor->search_action_circulation(
2604 {target_copy => $self->copy->id, usr => $usrid, stop_fines => undef})->[0];
2606 $circ = $self->editor->search_action_circulation(
2607 {target_copy => $self->copy->id, stop_fines => undef})->[0];
2612 $circ = $self->editor->search_action_circulation(
2613 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2615 $circ = $self->editor->search_action_circulation(
2616 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2620 return $self->bail_on_events($self->editor->event) unless $circ;
2622 # A user is not allowed to renew another user's items without permission
2623 unless( $circ->usr eq $self->editor->requestor->id ) {
2624 return $self->bail_on_events($self->editor->events)
2625 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2628 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2629 if $circ->renewal_remaining < 1;
2631 # -----------------------------------------------------------------
2633 $self->parent_circ($circ->id);
2634 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2637 $self->run_renew_permit;
2640 $self->do_checkin();
2641 return if $self->bail_out;
2643 unless( $self->permit_override ) {
2645 return if $self->bail_out;
2646 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2647 $self->remove_event('ITEM_NOT_CATALOGED');
2650 $self->override_events;
2651 return if $self->bail_out;
2654 $self->do_checkout();
2659 my( $self, $evt ) = @_;
2660 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2661 $logger->debug("circulator: removing event from list: $evt");
2662 my @events = @{$self->events};
2663 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2668 my( $self, $evt ) = @_;
2669 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2670 return grep { $_->{textcode} eq $evt } @{$self->events};
2675 sub run_renew_permit {
2680 if(!$self->legacy_script_support) {
2681 my $results = $self->run_indb_circ_test;
2682 unless($self->circ_test_success) {
2683 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) for @$results;
2688 my $runner = $self->script_runner;
2690 $runner->load($self->circ_permit_renew);
2691 my $result = $runner->run or
2692 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2693 $events = $result->{events};
2694 $self->mk_script_runner;
2697 $logger->activity("circulator: circ_permit_renew for user ".
2698 $self->patron->id." returned events: @$events") if @$events;
2700 $self->push_events(OpenILS::Event->new($_)) for @$events;
2702 $logger->debug("circulator: re-creating script runner to be safe");
2706 sub append_reading_list {
2710 $self->is_checkout and
2715 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2717 # verify history is globally enabled and uses the bucket mechanism
2718 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2719 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2721 unless($htype eq 'bucket') {
2726 # verify the patron wants to retain the hisory
2727 my $setting = $e->search_actor_user_setting(
2728 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2730 unless($setting and $setting->value) {
2735 my $bkt = $e->search_container_copy_bucket(
2736 {owner => $self->patron->id, btype => 'circ_history'})->[0];
2741 # find the next item position
2742 my $last_item = $e->search_container_copy_bucket_item(
2743 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2744 $pos = $last_item->pos + 1 if $last_item;
2747 # create the history bucket if necessary
2748 $bkt = Fieldmapper::container::copy_bucket->new;
2749 $bkt->owner($self->patron->id);
2751 $bkt->btype('circ_history');
2753 $e->create_container_copy_bucket($bkt) or return $e->die_event;
2756 my $item = Fieldmapper::container::copy_bucket_item->new;
2758 $item->bucket($bkt->id);
2759 $item->target_copy($self->copy->id);
2762 $e->create_container_copy_bucket_item($item) or return $e->die_event;
2769 sub make_trigger_events {
2771 return unless $self->circ;
2772 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2773 $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2774 $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2775 $ses->request('open-ils.trigger.event.autocreate', 'renewal', $self->circ, $self->circ_lib) if $self->is_renewal;
2782 sub checkin_handle_lost_now_found {
2783 my ($self, $bill_type) = @_;
2785 # ------------------------------------------------------------------
2786 # remove charge from patron's account if lost item is returned
2787 # ------------------------------------------------------------------
2789 my $bills = $self->editor->search_money_billing(
2791 xact => $self->circ->id,
2796 $logger->debug("voiding lost item charge of ".scalar(@$bills));
2797 for my $bill (@$bills) {
2798 if( !$U->is_true($bill->voided) ) {
2799 $logger->info("lost item returned - voiding bill ".$bill->id);
2801 $bill->void_time('now');
2802 $bill->voider($self->editor->requestor->id);
2803 my $note = ($bill->note) ? $bill->note . "\n" : '';
2804 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2806 $self->bail_on_events($self->editor->event)
2807 unless $self->editor->update_money_billing($bill);
2812 sub checkin_handle_lost_now_found_restore_od {
2815 # ------------------------------------------------------------------
2816 # restore those overdue charges voided when item was set to lost
2817 # ------------------------------------------------------------------
2819 my $ods = $self->editor->search_money_billing(
2821 xact => $self->circ->id,
2826 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
2827 for my $bill (@$ods) {
2828 if( $U->is_true($bill->voided) ) {
2829 $logger->info("lost item returned - restoring overdue ".$bill->id);
2831 $bill->clear_void_time;
2832 $bill->voider($self->editor->requestor->id);
2833 my $note = ($bill->note) ? $bill->note . "\n" : '';
2834 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2836 $self->bail_on_events($self->editor->event)
2837 unless $self->editor->update_money_billing($bill);