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
118 __PACKAGE__->register_method(
119 method => "run_method",
120 api_name => "open-ils.circ.checkin.override",
121 signature => q/@see open-ils.circ.checkin/
124 __PACKAGE__->register_method(
125 method => "run_method",
126 api_name => "open-ils.circ.renew.override",
127 signature => q/@see open-ils.circ.renew/,
131 __PACKAGE__->register_method(
132 method => "run_method",
133 api_name => "open-ils.circ.renew",
134 notes => <<" NOTES");
135 PARAMS( authtoken, circ => circ_id );
136 open-ils.circ.renew(login_session, circ_object);
137 Renews the provided circulation. login_session is the requestor of the
138 renewal and if the logged in user is not the same as circ->usr, then
139 the logged in user must have RENEW_CIRC permissions.
142 __PACKAGE__->register_method(
143 method => "run_method",
144 api_name => "open-ils.circ.checkout.full");
145 __PACKAGE__->register_method(
146 method => "run_method",
147 api_name => "open-ils.circ.checkout.full.override");
149 __PACKAGE__->register_method(
150 method => "run_method",
151 api_name => "open-ils.circ.checkout.inspect",
153 Returns the circ matrix test result and, on success, the rule set and matrix test object
160 my( $self, $conn, $auth, $args ) = @_;
161 translate_legacy_args($args);
162 my $api = $self->api_name;
165 OpenILS::Application::Circ::Circulator->new($auth, %$args);
167 return circ_events($circulator) if $circulator->bail_out;
169 # --------------------------------------------------------------------------
170 # Go ahead and load the script runner to make sure we have all
171 # of the objects we need
172 # --------------------------------------------------------------------------
173 $circulator->is_renewal(1) if $api =~ /renew/;
174 $circulator->is_checkin(1) if $api =~ /checkin/;
176 if($legacy_script_support and not $circulator->is_checkin) {
177 $circulator->mk_script_runner();
178 $circulator->legacy_script_support(1);
179 $circulator->circ_permit_patron($scripts{circ_permit_patron});
180 $circulator->circ_permit_copy($scripts{circ_permit_copy});
181 $circulator->circ_duration($scripts{circ_duration});
182 $circulator->circ_permit_renew($scripts{circ_permit_renew});
184 $circulator->mk_env();
186 return circ_events($circulator) if $circulator->bail_out;
189 $circulator->override(1) if $api =~ /override/o;
191 if( $api =~ /checkout\.permit/ ) {
192 $circulator->do_permit();
194 } elsif( $api =~ /checkout.full/ ) {
196 # requesting a precat checkout implies that any required
197 # overrides have been performed. Go ahead and re-override.
198 $circulator->override(1) if $circulator->request_precat;
199 $circulator->do_permit();
200 $circulator->is_checkout(1);
201 unless( $circulator->bail_out ) {
202 $circulator->events([]);
203 $circulator->do_checkout();
206 } elsif( $api =~ /inspect/ ) {
207 my $data = $circulator->do_inspect();
208 $circulator->editor->rollback;
211 } elsif( $api =~ /checkout/ ) {
212 $circulator->is_checkout(1);
213 $circulator->do_checkout();
215 } elsif( $api =~ /checkin/ ) {
216 $circulator->do_checkin();
218 } elsif( $api =~ /renew/ ) {
219 $circulator->is_renewal(1);
220 $circulator->do_renew();
223 if( $circulator->bail_out ) {
226 # make sure no success event accidentally slip in
228 [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
231 my @e = @{$circulator->events};
232 push( @ee, $_->{textcode} ) for @e;
233 $logger->info("circulator: bailing out with events: @ee");
235 $circulator->editor->rollback;
238 $circulator->editor->commit;
241 $circulator->script_runner->cleanup if $circulator->script_runner;
243 $conn->respond_complete(circ_events($circulator));
245 unless($circulator->bail_out) {
246 $circulator->do_hold_notify($circulator->notify_hold)
247 if $circulator->notify_hold;
248 $circulator->retarget_holds if $circulator->retarget;
249 $circulator->append_reading_list;
250 $circulator->make_trigger_events;
256 my @e = @{$circ->events};
257 # if we have multiple events, SUCCESS should not be one of them;
258 @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
259 return (@e == 1) ? $e[0] : \@e;
263 sub translate_legacy_args {
266 if( $$args{barcode} ) {
267 $$args{copy_barcode} = $$args{barcode};
268 delete $$args{barcode};
271 if( $$args{copyid} ) {
272 $$args{copy_id} = $$args{copyid};
273 delete $$args{copyid};
276 if( $$args{patronid} ) {
277 $$args{patron_id} = $$args{patronid};
278 delete $$args{patronid};
281 if( $$args{patron} and !ref($$args{patron}) ) {
282 $$args{patron_id} = $$args{patron};
283 delete $$args{patron};
287 if( $$args{noncat} ) {
288 $$args{is_noncat} = $$args{noncat};
289 delete $$args{noncat};
292 if( $$args{precat} ) {
293 $$args{is_precat} = $$args{request_precat} = $$args{precat};
294 delete $$args{precat};
300 # --------------------------------------------------------------------------
301 # This package actually manages all of the circulation logic
302 # --------------------------------------------------------------------------
303 package OpenILS::Application::Circ::Circulator;
304 use strict; use warnings;
305 use vars q/$AUTOLOAD/;
307 use OpenILS::Utils::Fieldmapper;
308 use OpenSRF::Utils::Cache;
309 use Digest::MD5 qw(md5_hex);
310 use DateTime::Format::ISO8601;
311 use OpenILS::Utils::PermitHold;
312 use OpenSRF::Utils qw/:datetime/;
313 use OpenSRF::Utils::SettingsClient;
314 use OpenILS::Application::Circ::Holds;
315 use OpenILS::Application::Circ::Transit;
316 use OpenSRF::Utils::Logger qw(:logger);
317 use OpenILS::Utils::CStoreEditor qw/:funcs/;
318 use OpenILS::Application::Circ::ScriptBuilder;
319 use OpenILS::Const qw/:const/;
320 use OpenILS::Utils::Penalty;
321 use OpenILS::Application::Circ::CircCommon;
324 my $holdcode = "OpenILS::Application::Circ::Holds";
325 my $transcode = "OpenILS::Application::Circ::Transit";
331 # --------------------------------------------------------------------------
332 # Add a pile of automagic getter/setter methods
333 # --------------------------------------------------------------------------
334 my @AUTOLOAD_FIELDS = qw/
376 recurring_fines_level
389 cancelled_hold_transit
395 circ_matrix_matchpoint
397 legacy_script_support
409 my $type = ref($self) or die "$self is not an object";
411 my $name = $AUTOLOAD;
414 unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
415 $logger->error("circulator: $type: invalid autoload field: $name");
416 die "$type: invalid autoload field: $name\n"
421 *{"${type}::${name}"} = sub {
424 $s->{$name} = $v if defined $v;
428 return $self->$name($data);
433 my( $class, $auth, %args ) = @_;
434 $class = ref($class) || $class;
435 my $self = bless( {}, $class );
438 $self->editor(new_editor(xact => 1, authtoken => $auth));
440 unless( $self->editor->checkauth ) {
441 $self->bail_on_events($self->editor->event);
445 $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
447 $self->$_($args{$_}) for keys %args;
450 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
452 # if this is a renewal, default to desk_renewal
453 $self->desk_renewal(1) unless
454 $self->opac_renewal or $self->phone_renewal;
456 $self->capture('') unless $self->capture;
458 unless(%user_groups) {
459 my $gps = $self->editor->retrieve_all_permission_grp_tree;
460 %user_groups = map { $_->id => $_ } @$gps;
467 # --------------------------------------------------------------------------
468 # True if we should discontinue processing
469 # --------------------------------------------------------------------------
471 my( $self, $bool ) = @_;
472 if( defined $bool ) {
473 $logger->info("circulator: BAILING OUT") if $bool;
474 $self->{bail_out} = $bool;
476 return $self->{bail_out};
481 my( $self, @evts ) = @_;
484 $logger->info("circulator: pushing event ".$e->{textcode});
485 push( @{$self->events}, $e ) unless
486 grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
492 my $key = md5_hex( time() . rand() . "$$" );
493 $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
494 return $self->permit_key($key);
497 sub check_permit_key {
499 my $key = $self->permit_key;
500 return 0 unless $key;
501 my $k = "oils_permit_key_$key";
502 my $one = $self->cache_handle->get_cache($k);
503 $self->cache_handle->delete_cache($k);
504 return ($one) ? 1 : 0;
509 my $e = $self->editor;
511 # --------------------------------------------------------------------------
512 # Grab the fleshed copy
513 # --------------------------------------------------------------------------
514 unless($self->is_noncat) {
518 flesh_fields => {acp => ['call_number'], acn => ['record']}
521 $copy = $e->retrieve_asset_copy(
522 [$self->copy_id, $flesh ]) or return $e->event;
524 } elsif( $self->copy_barcode ) {
526 $copy = $e->search_asset_copy(
527 [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
532 $self->volume($copy->call_number);
533 $self->title($self->volume->record);
534 $self->copy->call_number($self->volume->id);
535 $self->volume->record($self->title->id);
536 $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
537 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
538 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
539 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
542 # We can't renew if there is no copy
543 return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
544 if $self->is_renewal;
549 # --------------------------------------------------------------------------
551 # --------------------------------------------------------------------------
553 if( $self->patron_id ) {
554 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
556 } elsif( $self->patron_barcode ) {
558 my $card = $e->search_actor_card(
559 {barcode => $self->patron_barcode})->[0] or return $e->event;
561 $patron = $e->search_actor_user(
562 {card => $card->id})->[0] or return $e->event;
565 if( my $copy = $self->copy ) {
566 my $circs = $e->search_action_circulation(
567 {target_copy => $copy->id, checkin_time => undef});
569 if( my $circ = $circs->[0] ) {
570 $patron = $e->retrieve_actor_user($circ->usr)
576 return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
577 unless $self->patron($patron) or $self->is_checkin;
580 # --------------------------------------------------------------------------
581 # This builds the script runner environment and fetches most of the
583 # --------------------------------------------------------------------------
584 sub mk_script_runner {
590 qw/copy copy_barcode copy_id patron
591 patron_id patron_barcode volume title editor/;
593 # Translate our objects into the ScriptBuilder args hash
594 $$args{$_} = $self->$_() for @fields;
596 $args->{ignore_user_status} = 1 if $self->is_checkin;
597 $$args{fetch_patron_by_circ_copy} = 1;
598 $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
600 if( my $pco = $self->pending_checkouts ) {
601 $logger->info("circulator: we were given a pending checkouts number of $pco");
602 $$args{patronItemsOut} = $pco;
605 # This fetches most of the objects we need
606 $self->script_runner(
607 OpenILS::Application::Circ::ScriptBuilder->build($args));
609 # Now we translate the ScriptBuilder objects back into self
610 $self->$_($$args{$_}) for @fields;
612 my @evts = @{$args->{_events}} if $args->{_events};
614 $logger->debug("circulator: script builder returned events: @evts") if @evts;
618 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
619 if(!$self->is_noncat and
621 $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
625 my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
626 return $self->bail_on_events(@e);
631 $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
632 if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
633 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
634 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
638 # We can't renew if there is no copy
639 return $self->bail_on_events(@evts) if
640 $self->is_renewal and !$self->copy;
642 # Set some circ-specific flags in the script environment
643 my $evt = "environment";
644 $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
646 if( $self->is_noncat ) {
647 $self->script_runner->insert("$evt.isNonCat", 1);
648 $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
651 if( $self->is_precat ) {
652 $self->script_runner->insert("environment.isPrecat", 1, 1);
655 $self->script_runner->add_path( $_ ) for @$script_libs;
660 # --------------------------------------------------------------------------
661 # Does the circ permit work
662 # --------------------------------------------------------------------------
666 $self->log_me("do_permit()");
668 unless( $self->editor->requestor->id == $self->patron->id ) {
669 return $self->bail_on_events($self->editor->event)
670 unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
673 $self->check_captured_holds();
674 $self->do_copy_checks();
675 return if $self->bail_out;
676 $self->run_patron_permit_scripts();
677 $self->run_copy_permit_scripts()
678 unless $self->is_precat or $self->is_noncat;
679 $self->check_item_deposit_events();
680 $self->override_events();
681 return if $self->bail_out;
683 if($self->is_precat and not $self->request_precat) {
686 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
687 return $self->bail_out(1) unless $self->is_renewal;
691 OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
694 sub check_item_deposit_events {
696 $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy))
697 if $self->is_deposit and not $self->is_deposit_exempt;
698 $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy))
699 if $self->is_rental and not $self->is_rental_exempt;
702 # returns true if the user is not required to pay deposits
703 sub is_deposit_exempt {
705 my $pid = (ref $self->patron->profile) ?
706 $self->patron->profile->id : $self->patron->profile;
707 my $groups = $U->ou_ancestor_setting_value(
708 $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
709 for my $grp (@$groups) {
710 return 1 if $self->is_group_descendant($grp, $pid);
715 # returns true if the user is not required to pay rental fees
716 sub is_rental_exempt {
718 my $pid = (ref $self->patron->profile) ?
719 $self->patron->profile->id : $self->patron->profile;
720 my $groups = $U->ou_ancestor_setting_value(
721 $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
722 for my $grp (@$groups) {
723 return 1 if $self->is_group_descendant($grp, $pid);
728 sub is_group_descendant {
729 my($self, $p_id, $c_id) = @_;
730 return 0 unless defined $p_id and defined $c_id;
731 return 1 if $c_id == $p_id;
732 while(my $grp = $user_groups{$c_id}) {
733 $c_id = $grp->parent;
734 return 0 unless defined $c_id;
735 return 1 if $c_id == $p_id;
740 sub check_captured_holds {
742 my $copy = $self->copy;
743 my $patron = $self->patron;
745 return undef unless $copy;
747 my $s = $U->copy_status($copy->status)->id;
748 return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
749 $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
751 # Item is on the holds shelf, make sure it's going to the right person
752 my $holds = $self->editor->search_action_hold_request(
755 current_copy => $copy->id ,
756 capture_time => { '!=' => undef },
757 cancel_time => undef,
758 fulfillment_time => undef
764 if( $holds and $$holds[0] ) {
765 return undef if $$holds[0]->usr == $patron->id;
768 $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
770 $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
776 my $copy = $self->copy;
779 my $stat = $U->copy_status($copy->status)->id;
781 # We cannot check out a copy if it is in-transit
782 if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
783 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
786 $self->handle_claims_returned();
787 return if $self->bail_out;
789 # no claims returned circ was found, check if there is any open circ
790 unless( $self->is_renewal ) {
791 my $circs = $self->editor->search_action_circulation(
792 { target_copy => $copy->id, checkin_time => undef }
795 return $self->bail_on_events(
796 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
800 my $LEGACY_CIRC_EVENT_MAP = {
801 'actor.usr.barred' => 'PATRON_BARRED',
802 'asset.copy.circulate' => 'COPY_CIRC_NOT_ALLOWED',
803 'asset.copy.status' => 'COPY_NOT_AVAILABLE',
804 'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
805 'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
806 'config.circ_matrix_test.max_items_out' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
807 'config.circ_matrix_test.max_overdue' => 'PATRON_EXCEEDS_OVERDUE_COUNT',
808 'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
809 'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
813 # ---------------------------------------------------------------------
814 # This pushes any patron-related events into the list but does not
815 # set bail_out for any events
816 # ---------------------------------------------------------------------
817 sub run_patron_permit_scripts {
819 my $runner = $self->script_runner;
820 my $patronid = $self->patron->id;
824 if(!$self->legacy_script_support) {
826 my $results = $self->run_indb_circ_test;
827 unless($self->circ_test_success) {
828 push(@allevents, OpenILS::Event->new(
829 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
835 # --------------------------------------------------------------------- # Now run the patron permit script
836 # ---------------------------------------------------------------------
837 $runner->load($self->circ_permit_patron);
838 my $result = $runner->run or
839 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
841 my $patron_events = $result->{events};
843 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
844 my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
845 my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
846 $penalties = $penalties->{fatal_penalties};
848 for my $pen (@$penalties) {
849 my $event = OpenILS::Event->new($pen->name);
850 $event->{desc} = $pen->label;
851 push(@allevents, $event);
854 push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
857 $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
859 $self->push_events(@allevents);
862 sub run_indb_circ_test {
864 return $self->matrix_test_result if $self->matrix_test_result;
866 my $dbfunc = ($self->is_renewal) ?
867 'action.item_user_renew_test' : 'action.item_user_circ_test';
869 my $results = ($self->matrix_test_result) ?
870 $self->matrix_test_result :
871 $self->editor->json_query(
874 $self->editor->requestor->ws_ou,
875 ($self->is_precat or $self->is_noncat) ? undef : $self->copy->id,
881 $self->circ_test_success($U->is_true($results->[0]->{success}));
883 if(my $mp = $results->[0]->{matchpoint}) {
884 $self->circ_matrix_matchpoint(
885 $self->editor->retrieve_config_circ_matrix_matchpoint([
888 flesh_fields => {ccmm =>
889 ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
895 return $self->matrix_test_result($results);
900 $self->run_indb_circ_test;
903 circ_test_success => $self->circ_test_success,
904 failure_events => [],
908 unless($self->circ_test_success) {
909 push(@{$results->{failure_codes}},
910 $_->{fail_part}) for @{$self->matrix_test_result};
911 push(@{$results->{failure_events}},
912 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}})
913 for @{$self->matrix_test_result};
917 my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
918 my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
919 my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
921 my $policy = $self->get_circ_policy(
922 $duration_rule, $recurring_fine_rule, $max_fine_rule);
924 $$results{$_} = $$policy{$_} for keys %$policy;
928 # ---------------------------------------------------------------------
929 # Loads the circ policy info for duration, recurring fine, and max
930 # fine based on the current copy
931 # ---------------------------------------------------------------------
932 sub get_circ_policy {
933 my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
936 duration_rule => $duration_rule->name,
937 recurring_fine_rule => $recurring_fine_rule->name,
938 max_fine_rule => $max_fine_rule->name,
939 max_fine => $self->get_max_fine_amount($max_fine_rule),
940 fine_interval => $recurring_fine_rule->recurance_interval,
941 renewal_remaining => $duration_rule->max_renewals
944 $policy->{duration} = $duration_rule->shrt
945 if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
946 $policy->{duration} = $duration_rule->normal
947 if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
948 $policy->{duration} = $duration_rule->extended
949 if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
951 $policy->{recurring_fine} = $recurring_fine_rule->low
952 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
953 $policy->{recurring_fine} = $recurring_fine_rule->normal
954 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
955 $policy->{recurring_fine} = $recurring_fine_rule->high
956 if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
961 sub get_max_fine_amount {
963 my $max_fine_rule = shift;
964 my $max_amount = $max_fine_rule->amount;
966 # if is_percent is true then the max->amount is
967 # use as a percentage of the copy price
968 if ($U->is_true($max_fine_rule->is_percent)) {
969 my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
970 $max_amount = $price * $max_fine_rule->amount / 100;
978 sub run_copy_permit_scripts {
980 my $copy = $self->copy || return;
981 my $runner = $self->script_runner;
985 if(!$self->legacy_script_support) {
986 my $results = $self->run_indb_circ_test;
987 unless($self->circ_test_success) {
988 push(@allevents, OpenILS::Event->new(
989 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
994 # ---------------------------------------------------------------------
995 # Capture all of the copy permit events
996 # ---------------------------------------------------------------------
997 $runner->load($self->circ_permit_copy);
998 my $result = $runner->run or
999 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1000 my $copy_events = $result->{events};
1002 # ---------------------------------------------------------------------
1003 # Now collect all of the events together
1004 # ---------------------------------------------------------------------
1005 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1008 # See if this copy has an alert message
1009 my $ae = $self->check_copy_alert();
1010 push( @allevents, $ae ) if $ae;
1012 # uniquify the events
1013 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1014 @allevents = values %hash;
1017 $_->{payload} = $copy if
1018 ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1021 $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1023 $self->push_events(@allevents);
1027 sub check_copy_alert {
1029 return undef if $self->is_renewal;
1030 return OpenILS::Event->new(
1031 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1032 if $self->copy and $self->copy->alert_message;
1038 # --------------------------------------------------------------------------
1039 # If the call is overriding and has permissions to override every collected
1040 # event, the are cleared. Any event that the caller does not have
1041 # permission to override, will be left in the event list and bail_out will
1043 # XXX We need code in here to cancel any holds/transits on copies
1044 # that are being force-checked out
1045 # --------------------------------------------------------------------------
1046 sub override_events {
1048 my @events = @{$self->events};
1049 return unless @events;
1051 if(!$self->override) {
1052 return $self->bail_out(1)
1053 if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1058 for my $e (@events) {
1059 my $tc = $e->{textcode};
1060 next if $tc eq 'SUCCESS';
1061 my $ov = "$tc.override";
1062 $logger->info("circulator: attempting to override event: $ov");
1064 return $self->bail_on_events($self->editor->event)
1065 unless( $self->editor->allowed($ov) );
1070 # --------------------------------------------------------------------------
1071 # If there is an open claimsreturn circ on the requested copy, close the
1072 # circ if overriding, otherwise bail out
1073 # --------------------------------------------------------------------------
1074 sub handle_claims_returned {
1076 my $copy = $self->copy;
1078 my $CR = $self->editor->search_action_circulation(
1080 target_copy => $copy->id,
1081 stop_fines => OILS_STOP_FINES_CLAIMSRETURNED,
1082 checkin_time => undef,
1086 return unless ($CR = $CR->[0]);
1090 # - If the caller has set the override flag, we will check the item in
1091 if($self->override) {
1093 $CR->checkin_time('now');
1094 $CR->checkin_lib($self->editor->requestor->ws_ou);
1095 $CR->checkin_staff($self->editor->requestor->id);
1097 $evt = $self->editor->event
1098 unless $self->editor->update_action_circulation($CR);
1101 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1104 $self->bail_on_events($evt) if $evt;
1109 # --------------------------------------------------------------------------
1110 # This performs the checkout
1111 # --------------------------------------------------------------------------
1115 $self->log_me("do_checkout()");
1117 # make sure perms are good if this isn't a renewal
1118 unless( $self->is_renewal ) {
1119 return $self->bail_on_events($self->editor->event)
1120 unless( $self->editor->allowed('COPY_CHECKOUT') );
1123 # verify the permit key
1124 unless( $self->check_permit_key ) {
1125 if( $self->permit_override ) {
1126 return $self->bail_on_events($self->editor->event)
1127 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1129 return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1133 # if this is a non-cataloged circ, build the circ and finish
1134 if( $self->is_noncat ) {
1135 $self->checkout_noncat;
1137 OpenILS::Event->new('SUCCESS',
1138 payload => { noncat_circ => $self->circ }));
1142 if( $self->is_precat ) {
1143 $self->make_precat_copy;
1144 return if $self->bail_out;
1146 } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1147 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1150 $self->do_copy_checks;
1151 return if $self->bail_out;
1153 $self->run_checkout_scripts();
1154 return if $self->bail_out;
1156 $self->build_checkout_circ_object();
1157 return if $self->bail_out;
1159 $self->apply_modified_due_date();
1160 return if $self->bail_out;
1162 return $self->bail_on_events($self->editor->event)
1163 unless $self->editor->create_action_circulation($self->circ);
1165 # refresh the circ to force local time zone for now
1166 $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1168 $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1170 return if $self->bail_out;
1172 $self->apply_deposit_fee();
1173 return if $self->bail_out;
1175 $self->handle_checkout_holds();
1176 return if $self->bail_out;
1178 # ------------------------------------------------------------------------------
1179 # Update the patron penalty info in the DB. Run it for permit-overrides
1180 # since the penalties are not updated during the permit phase
1181 # ------------------------------------------------------------------------------
1182 OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1184 my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1186 OpenILS::Event->new('SUCCESS',
1188 copy => $U->unflesh_copy($self->copy),
1189 circ => $self->circ,
1191 holds_fulfilled => $self->fulfilled_holds,
1192 deposit_billing => $self->deposit_billing,
1193 rental_billing => $self->rental_billing
1199 sub apply_deposit_fee {
1201 my $copy = $self->copy;
1203 ($self->is_deposit and not $self->is_deposit_exempt) or
1204 ($self->is_rental and not $self->is_rental_exempt);
1206 my $bill = Fieldmapper::money::billing->new;
1207 my $amount = $copy->deposit_amount;
1211 if($self->is_deposit) {
1212 $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1214 $self->deposit_billing($bill);
1216 $billing_type = OILS_BILLING_TYPE_RENTAL;
1218 $self->rental_billing($bill);
1221 $bill->xact($self->circ->id);
1222 $bill->amount($amount);
1223 $bill->note(OILS_BILLING_NOTE_SYSTEM);
1224 $bill->billing_type($billing_type);
1225 $bill->btype($btype);
1226 $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1228 $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1233 my $copy = $self->copy;
1235 my $stat = $copy->status if ref $copy->status;
1236 my $loc = $copy->location if ref $copy->location;
1237 my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1239 $copy->status($stat->id) if $stat;
1240 $copy->location($loc->id) if $loc;
1241 $copy->circ_lib($circ_lib->id) if $circ_lib;
1242 $copy->editor($self->editor->requestor->id);
1243 $copy->edit_date('now');
1244 $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1246 return $self->bail_on_events($self->editor->event)
1247 unless $self->editor->update_asset_copy($self->copy);
1249 $copy->status($U->copy_status($copy->status));
1250 $copy->location($loc) if $loc;
1251 $copy->circ_lib($circ_lib) if $circ_lib;
1255 sub bail_on_events {
1256 my( $self, @evts ) = @_;
1257 $self->push_events(@evts);
1261 sub handle_checkout_holds {
1264 my $copy = $self->copy;
1265 my $patron = $self->patron;
1267 my $holds = $self->editor->search_action_hold_request(
1269 current_copy => $copy->id ,
1270 cancel_time => undef,
1271 fulfillment_time => undef
1277 # XXX We should only fulfill one hold here...
1278 # XXX If a hold was transited to the user who is checking out
1279 # the item, we need to make sure that hold is what's grabbed
1282 # for now, just sort by id to get what should be the oldest hold
1283 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1284 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1285 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1288 my $hold = $myholds[0];
1290 $logger->debug("circulator: related hold found in checkout: " . $hold->id );
1292 # if the hold was never officially captured, capture it.
1293 $hold->capture_time('now') unless $hold->capture_time;
1295 # just make sure it's set correctly
1296 $hold->current_copy($copy->id);
1298 $hold->fulfillment_time('now');
1299 $hold->fulfillment_staff($self->editor->requestor->id);
1300 $hold->fulfillment_lib($self->editor->requestor->ws_ou);
1302 return $self->bail_on_events($self->editor->event)
1303 unless $self->editor->update_action_hold_request($hold);
1305 $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
1307 push( @fulfilled, $hold->id );
1310 # If there are any holds placed for other users that point to this copy,
1311 # then we need to un-target those holds so the targeter can pick a new copy
1314 $logger->info("circulator: un-targeting hold ".$_->id.
1315 " because copy ".$copy->id." is getting checked out");
1317 # - make the targeter process this hold at next run
1318 $_->clear_prev_check_time;
1320 # - clear out the targetted copy
1321 $_->clear_current_copy;
1322 $_->clear_capture_time;
1324 return $self->bail_on_event($self->editor->event)
1325 unless $self->editor->update_action_hold_request($_);
1329 $self->fulfilled_holds(\@fulfilled);
1334 sub run_checkout_scripts {
1338 my $runner = $self->script_runner;
1347 if(!$self->legacy_script_support) {
1348 $self->run_indb_circ_test();
1349 $duration = $self->circ_matrix_matchpoint->duration_rule;
1350 $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1351 $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1355 $runner->load($self->circ_duration);
1357 my $result = $runner->run or
1358 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1360 $duration_name = $result->{durationRule};
1361 $recurring_name = $result->{recurringFinesRule};
1362 $max_fine_name = $result->{maxFine};
1365 $duration_name = $duration->name if $duration;
1366 if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1369 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1370 return $self->bail_on_events($evt) if $evt;
1372 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1373 return $self->bail_on_events($evt) if $evt;
1375 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1376 return $self->bail_on_events($evt) if $evt;
1381 # The item circulates with an unlimited duration
1387 $self->duration_rule($duration);
1388 $self->recurring_fines_rule($recurring);
1389 $self->max_fine_rule($max_fine);
1393 sub build_checkout_circ_object {
1396 my $circ = Fieldmapper::action::circulation->new;
1397 my $duration = $self->duration_rule;
1398 my $max = $self->max_fine_rule;
1399 my $recurring = $self->recurring_fines_rule;
1400 my $copy = $self->copy;
1401 my $patron = $self->patron;
1405 my $policy = $self->get_circ_policy($duration, $recurring, $max);
1407 my $dname = $duration->name;
1408 my $mname = $max->name;
1409 my $rname = $recurring->name;
1411 $logger->debug("circulator: building circulation ".
1412 "with duration=$dname, maxfine=$mname, recurring=$rname");
1414 $circ->duration($policy->{duration});
1415 $circ->recuring_fine($policy->{recurring_fine});
1416 $circ->duration_rule($duration->name);
1417 $circ->recuring_fine_rule($recurring->name);
1418 $circ->max_fine_rule($max->name);
1419 $circ->max_fine($policy->{max_fine});
1420 $circ->fine_interval($recurring->recurance_interval);
1421 $circ->renewal_remaining($duration->max_renewals);
1425 $logger->info("circulator: copy found with an unlimited circ duration");
1426 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1427 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1428 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1429 $circ->renewal_remaining(0);
1432 $circ->target_copy( $copy->id );
1433 $circ->usr( $patron->id );
1434 $circ->circ_lib( $self->circ_lib );
1436 if( $self->is_renewal ) {
1437 $circ->opac_renewal('t') if $self->opac_renewal;
1438 $circ->phone_renewal('t') if $self->phone_renewal;
1439 $circ->desk_renewal('t') if $self->desk_renewal;
1440 $circ->renewal_remaining($self->renewal_remaining);
1441 $circ->circ_staff($self->editor->requestor->id);
1445 # if the user provided an overiding checkout time,
1446 # (e.g. the checkout really happened several hours ago), then
1447 # we apply that here. Does this need a perm??
1448 $circ->xact_start(clense_ISO8601($self->checkout_time))
1449 if $self->checkout_time;
1451 # if a patron is renewing, 'requestor' will be the patron
1452 $circ->circ_staff($self->editor->requestor->id);
1453 $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1459 sub apply_modified_due_date {
1461 my $circ = $self->circ;
1462 my $copy = $self->copy;
1464 if( $self->due_date ) {
1466 return $self->bail_on_events($self->editor->event)
1467 unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1469 $circ->due_date(clense_ISO8601($self->due_date));
1473 # if the due_date lands on a day when the location is closed
1474 return unless $copy and $circ->due_date;
1476 #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1478 # due-date overlap should be determined by the location the item
1479 # is checked out from, not the owning or circ lib of the item
1480 my $org = $self->editor->requestor->ws_ou;
1482 $logger->info("circulator: circ searching for closed date overlap on lib $org".
1483 " with an item due date of ".$circ->due_date );
1485 my $dateinfo = $U->storagereq(
1486 'open-ils.storage.actor.org_unit.closed_date.overlap',
1487 $org, $circ->due_date );
1490 $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1491 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1493 # XXX make the behavior more dynamic
1494 # for now, we just push the due date to after the close date
1495 $circ->due_date($dateinfo->{end});
1502 sub create_due_date {
1503 my( $self, $duration ) = @_;
1504 # if there is a raw time component (e.g. from postgres),
1505 # turn it into an interval that interval_to_seconds can parse
1506 $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1507 my ($sec,$min,$hour,$mday,$mon,$year) =
1508 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1509 $year += 1900; $mon += 1;
1510 my $due_date = sprintf(
1511 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1512 $year, $mon, $mday, $hour, $min, $sec);
1518 sub make_precat_copy {
1520 my $copy = $self->copy;
1523 $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1525 $copy->editor($self->editor->requestor->id);
1526 $copy->edit_date('now');
1527 $copy->dummy_title($self->dummy_title);
1528 $copy->dummy_author($self->dummy_author);
1530 $self->update_copy();
1534 $logger->info("circulator: Creating a new precataloged ".
1535 "copy in checkout with barcode " . $self->copy_barcode);
1537 $copy = Fieldmapper::asset::copy->new;
1538 $copy->circ_lib($self->circ_lib);
1539 $copy->creator($self->editor->requestor->id);
1540 $copy->editor($self->editor->requestor->id);
1541 $copy->barcode($self->copy_barcode);
1542 $copy->call_number(OILS_PRECAT_CALL_NUMBER);
1543 $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1544 $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1546 $copy->dummy_title($self->dummy_title || "");
1547 $copy->dummy_author($self->dummy_author || "");
1549 unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1551 $self->push_events($self->editor->event);
1555 # this is a little bit of a hack, but we need to
1556 # get the copy into the script runner
1557 $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1561 sub checkout_noncat {
1567 my $lib = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1568 my $count = $self->noncat_count || 1;
1569 my $cotime = clense_ISO8601($self->checkout_time) || "";
1571 $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1575 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1576 $self->editor->requestor->id,
1584 $self->push_events($evt);
1595 $self->log_me("do_checkin()");
1597 return $self->bail_on_events(
1598 OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
1601 if( $self->checkin_check_holds_shelf() ) {
1602 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1603 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1604 $self->checkin_flesh_events;
1608 unless( $self->is_renewal ) {
1609 return $self->bail_on_events($self->editor->event)
1610 unless $self->editor->allowed('COPY_CHECKIN');
1613 $self->push_events($self->check_copy_alert());
1614 $self->push_events($self->check_checkin_copy_status());
1616 # the renew code will have already found our circulation object
1617 unless( $self->is_renewal and $self->circ ) {
1618 my $circs = $self->editor->search_action_circulation(
1619 { target_copy => $self->copy->id, checkin_time => undef });
1620 $self->circ($$circs[0]);
1622 # for now, just warn if there are multiple open circs on a copy
1623 $logger->warn("circulator: we have ".scalar(@$circs).
1624 " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1627 # if the circ is marked as 'claims returned', add the event to the list
1628 $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1629 if ($self->circ and $self->circ->stop_fines
1630 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1632 $self->check_circ_deposit();
1634 # handle the overridable events
1635 $self->override_events unless $self->is_renewal;
1636 return if $self->bail_out;
1640 $self->editor->search_action_transit_copy(
1641 { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);
1645 $self->checkin_handle_circ;
1646 return if $self->bail_out;
1647 $self->checkin_changed(1);
1649 } elsif( $self->transit ) {
1650 my $hold_transit = $self->process_received_transit;
1651 $self->checkin_changed(1);
1653 if( $self->bail_out ) {
1654 $self->checkin_flesh_events;
1658 if( my $e = $self->check_checkin_copy_status() ) {
1659 # If the original copy status is special, alert the caller
1660 my $ev = $self->events;
1661 $self->events([$e]);
1662 $self->override_events;
1663 return if $self->bail_out;
1667 if( $hold_transit or
1668 $U->copy_status($self->copy->status)->id
1669 == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1672 if( $hold_transit ) {
1673 $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1675 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1680 if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1682 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1683 $self->reshelve_copy(1);
1684 $self->cancelled_hold_transit(1);
1685 $self->notify_hold(0); # don't notify for cancelled holds
1686 return if $self->bail_out;
1690 # hold transited to correct location
1691 $self->checkin_flesh_events;
1696 } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1698 $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1699 " that is in-transit, but there is no transit.. repairing");
1700 $self->reshelve_copy(1);
1701 return if $self->bail_out;
1704 if( $self->is_renewal ) {
1705 $self->push_events(OpenILS::Event->new('SUCCESS'));
1709 # ------------------------------------------------------------------------------
1710 # Circulations and transits are now closed where necessary. Now go on to see if
1711 # this copy can fulfill a hold or needs to be routed to a different location
1712 # ------------------------------------------------------------------------------
1714 unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1716 my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1717 return if $self->bail_out;
1719 unless($needed_for_hold) {
1720 my $circ_lib = (ref $self->copy->circ_lib) ?
1721 $self->copy->circ_lib->id : $self->copy->circ_lib;
1723 if( $self->remote_hold ) {
1724 $circ_lib = $self->remote_hold->pickup_lib;
1725 $logger->warn("circulator: Copy ".$self->copy->barcode.
1726 " is on a remote hold's shelf, sending to $circ_lib");
1729 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1731 if( $circ_lib == $self->editor->requestor->ws_ou ) {
1733 $self->checkin_handle_precat();
1734 return if $self->bail_out;
1738 my $bc = $self->copy->barcode;
1739 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1740 $self->checkin_build_copy_transit($circ_lib);
1741 return if $self->bail_out;
1742 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1747 $self->reshelve_copy;
1748 return if $self->bail_out;
1750 unless($self->checkin_changed) {
1752 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1753 my $stat = $U->copy_status($self->copy->status)->id;
1755 $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1756 if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1757 $self->bail_out(1); # no need to commit anything
1761 $self->push_events(OpenILS::Event->new('SUCCESS'))
1762 unless @{$self->events};
1765 OpenILS::Utils::Penalty->calculate_penalties(
1766 $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1768 $self->checkin_flesh_events;
1772 # if a deposit was payed for this item, push the event
1773 sub check_circ_deposit {
1775 return unless $self->circ;
1776 my $deposit = $self->editor->search_money_billing(
1778 xact => $self->circ->id,
1780 }, {idlist => 1})->[0];
1782 $self->push_events(OpenILS::Event->new(
1783 'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1788 my $force = $self->force || shift;
1789 my $copy = $self->copy;
1791 my $stat = $U->copy_status($copy->status)->id;
1794 $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1795 $stat != OILS_COPY_STATUS_CATALOGING and
1796 $stat != OILS_COPY_STATUS_IN_TRANSIT and
1797 $stat != OILS_COPY_STATUS_RESHELVING )) {
1799 $copy->status( OILS_COPY_STATUS_RESHELVING );
1801 $self->checkin_changed(1);
1806 # Returns true if the item is at the current location
1807 # because it was transited there for a hold and the
1808 # hold has not been fulfilled
1809 sub checkin_check_holds_shelf {
1811 return 0 unless $self->copy;
1814 $U->copy_status($self->copy->status)->id ==
1815 OILS_COPY_STATUS_ON_HOLDS_SHELF;
1817 # find the hold that put us on the holds shelf
1818 my $holds = $self->editor->search_action_hold_request(
1820 current_copy => $self->copy->id,
1821 capture_time => { '!=' => undef },
1822 fulfillment_time => undef,
1823 cancel_time => undef,
1828 $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1829 $self->reshelve_copy(1);
1833 my $hold = $$holds[0];
1835 $logger->info("circulator: we found a captured, un-fulfilled hold [".
1836 $hold->id. "] for copy ".$self->copy->barcode);
1838 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1839 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1843 $logger->info("circulator: hold is not for here..");
1844 $self->remote_hold($hold);
1849 sub checkin_handle_precat {
1851 my $copy = $self->copy;
1853 if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1854 $copy->status(OILS_COPY_STATUS_CATALOGING);
1855 $self->update_copy();
1856 $self->checkin_changed(1);
1857 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1862 sub checkin_build_copy_transit {
1865 my $copy = $self->copy;
1866 my $transit = Fieldmapper::action::transit_copy->new;
1868 #$dest ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1869 $logger->info("circulator: transiting copy to $dest");
1871 $transit->source($self->editor->requestor->ws_ou);
1872 $transit->dest($dest);
1873 $transit->target_copy($copy->id);
1874 $transit->source_send_time('now');
1875 $transit->copy_status( $U->copy_status($copy->status)->id );
1877 $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1879 return $self->bail_on_events($self->editor->event)
1880 unless $self->editor->create_action_transit_copy($transit);
1882 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1884 $self->checkin_changed(1);
1888 # returns true if the item was used (or may potentially be used
1889 # in subsequent calls) to capture a hold.
1890 sub attempt_checkin_hold_capture {
1892 my $copy = $self->copy;
1894 # we've been explicitly told not to capture any holds
1895 return 0 if $self->capture eq 'nocapture';
1897 # See if this copy can fulfill any holds
1898 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1899 $self->editor, $copy, $self->editor->requestor );
1902 $logger->debug("circulator: no potential permitted".
1903 "holds found for copy ".$copy->barcode);
1907 if($self->capture ne 'capture') {
1908 # see if this item is in a hold-capture-delay location
1909 my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
1910 if($U->is_true($location->hold_verify)) {
1911 $self->bail_on_events(
1912 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
1917 $self->retarget($retarget);
1919 $logger->info("circulator: found permitted hold ".
1920 $hold->id . " for copy, capturing...");
1922 $hold->current_copy($copy->id);
1923 $hold->capture_time('now');
1925 # prevent DB errors caused by fetching
1926 # holds from storage, and updating through cstore
1927 $hold->clear_fulfillment_time;
1928 $hold->clear_fulfillment_staff;
1929 $hold->clear_fulfillment_lib;
1930 $hold->clear_expire_time;
1931 $hold->clear_cancel_time;
1932 $hold->clear_prev_check_time unless $hold->prev_check_time;
1934 $self->bail_on_events($self->editor->event)
1935 unless $self->editor->update_action_hold_request($hold);
1937 $self->checkin_changed(1);
1939 return 0 if $self->bail_out;
1941 if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1943 # This hold was captured in the correct location
1944 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1945 $self->push_events(OpenILS::Event->new('SUCCESS'));
1947 #$self->do_hold_notify($hold->id);
1948 $self->notify_hold($hold->id);
1952 # Hold needs to be picked up elsewhere. Build a hold
1953 # transit and route the item.
1954 $self->checkin_build_hold_transit();
1955 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1956 return 0 if $self->bail_out;
1957 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1960 # make sure we save the copy status
1965 sub do_hold_notify {
1966 my( $self, $holdid ) = @_;
1968 $logger->info("circulator: running delayed hold notify process");
1970 # my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1971 # hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1973 my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1974 hold_id => $holdid, requestor => $self->editor->requestor);
1976 $logger->debug("circulator: built hold notifier");
1978 if(!$notifier->event) {
1980 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1982 my $stat = $notifier->send_email_notify;
1983 if( $stat == '1' ) {
1984 $logger->info("ciculator: hold notify succeeded for hold $holdid");
1988 $logger->warn("ciculator: * hold notify failed for hold $holdid");
1991 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1995 sub retarget_holds {
1996 $logger->info("circulator: retargeting prev_check_time=null holds after opportunistic capture");
1997 my $ses = OpenSRF::AppSession->create('open-ils.storage');
1998 $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1999 # no reason to wait for the return value
2003 sub checkin_build_hold_transit {
2006 my $copy = $self->copy;
2007 my $hold = $self->hold;
2008 my $trans = Fieldmapper::action::hold_transit_copy->new;
2010 $logger->debug("circulator: building hold transit for ".$copy->barcode);
2012 $trans->hold($hold->id);
2013 $trans->source($self->editor->requestor->ws_ou);
2014 $trans->dest($hold->pickup_lib);
2015 $trans->source_send_time("now");
2016 $trans->target_copy($copy->id);
2018 # when the copy gets to its destination, it will recover
2019 # this status - put it onto the holds shelf
2020 $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2022 return $self->bail_on_events($self->editor->event)
2023 unless $self->editor->create_action_hold_transit_copy($trans);
2028 sub process_received_transit {
2030 my $copy = $self->copy;
2031 my $copyid = $self->copy->id;
2033 my $status_name = $U->copy_status($copy->status)->name;
2034 $logger->debug("circulator: attempting transit receive on ".
2035 "copy $copyid. Copy status is $status_name");
2037 my $transit = $self->transit;
2039 if( $transit->dest != $self->editor->requestor->ws_ou ) {
2040 # - this item is in-transit to a different location
2042 my $tid = $transit->id;
2043 my $loc = $self->editor->requestor->ws_ou;
2044 my $dest = $transit->dest;
2046 $logger->info("circulator: Fowarding transit on copy which is destined ".
2047 "for a different location. transit=$tid, copy=$copyid, current ".
2048 "location=$loc, destination location=$dest");
2050 my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2052 # grab the associated hold object if available
2053 my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2054 $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2056 return $self->bail_on_events($evt);
2059 # The transit is received, set the receive time
2060 $transit->dest_recv_time('now');
2061 $self->bail_on_events($self->editor->event)
2062 unless $self->editor->update_action_transit_copy($transit);
2064 my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2066 $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2067 $copy->status( $transit->copy_status );
2068 $self->update_copy();
2069 return if $self->bail_out;
2073 #$self->do_hold_notify($hold_transit->hold);
2074 $self->notify_hold($hold_transit->hold);
2079 OpenILS::Event->new(
2082 payload => { transit => $transit, holdtransit => $hold_transit } ));
2084 return $hold_transit;
2088 sub checkin_handle_circ {
2090 my $circ = $self->circ;
2091 my $copy = $self->copy;
2095 # backdate the circ if necessary
2096 if($self->backdate) {
2097 $self->checkin_handle_backdate;
2098 return if $self->bail_out;
2101 if(!$circ->stop_fines) {
2102 $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2103 $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2104 $circ->stop_fines_time('now') unless $self->backdate;
2105 $circ->stop_fines_time($self->backdate) if $self->backdate;
2108 # see if there are any fines owed on this circ. if not, close it
2109 ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2110 $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2112 $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2114 # Set the checkin vars since we have the item
2115 $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2117 $circ->checkin_staff($self->editor->requestor->id);
2118 $circ->checkin_lib($self->editor->requestor->ws_ou);
2120 my $circ_lib = (ref $self->copy->circ_lib) ?
2121 $self->copy->circ_lib->id : $self->copy->circ_lib;
2122 my $stat = $U->copy_status($self->copy->status)->id;
2124 # immediately available keeps items lost or missing items from going home before being handled
2125 my $lost_immediately_available = $U->ou_ancestor_setting_value(
2126 $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2129 if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2131 if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2132 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2134 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2138 } elsif ($stat == OILS_COPY_STATUS_LOST) {
2140 $self->checkin_handle_lost($circ_lib);
2144 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2148 return $self->bail_on_events($self->editor->event)
2149 unless $self->editor->update_action_circulation($circ);
2151 # make sure the circ isn't closed if we just voided some fines
2152 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2153 return $self->bail_on_events($evt) if $evt;
2159 # ------------------------------------------------------------------
2160 # See if we need to void billings for lost checkin
2161 # ------------------------------------------------------------------
2162 sub checkin_handle_lost {
2164 my $circ_lib = shift;
2165 my $circ = $self->circ;
2167 my $max_return = $U->ou_ancestor_setting_value(
2168 $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2173 my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2174 $tm[5] -= 1 if $tm[5] > 0;
2175 my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2177 my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2178 $logger->info("MAX OD: ".$max_return." DUEDATE: ".$circ->due_date." TODAY: ".$today." DUE: ".$due." LAST: ".$last_chance);
2180 $max_return = 0 if $today < $last_chance;
2183 if (!$max_return){ # there's either no max time to accept returns defined or we're within that time
2185 my $void_lost = $U->ou_ancestor_setting_value(
2186 $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2187 my $void_lost_fee = $U->ou_ancestor_setting_value(
2188 $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2189 my $restore_od = $U->ou_ancestor_setting_value(
2190 $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2192 $self->checkin_handle_lost_now_found(3) if $void_lost;
2193 $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2194 $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2197 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2202 sub checkin_handle_backdate {
2205 my $bd = $self->backdate;
2207 # ------------------------------------------------------------------
2208 # clean up the backdate for date comparison
2209 # we want any bills created on or after the backdate
2210 # ------------------------------------------------------------------
2211 $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2212 #$bd = "${bd}T23:59:59";
2214 my $bills = $self->editor->search_money_billing(
2216 billing_ts => { '>=' => $bd },
2217 xact => $self->circ->id,
2222 $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2224 for my $bill (@$bills) {
2225 unless( $U->is_true($bill->voided) ) {
2226 $logger->info("backdate voiding bill ".$bill->id);
2228 $bill->void_time('now');
2229 $bill->voider($self->editor->requestor->id);
2230 my $n = $bill->note || "";
2231 $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2233 $self->bail_on_events($self->editor->event)
2234 unless $self->editor->update_money_billing($bill);
2242 sub find_patron_from_copy {
2244 my $circs = $self->editor->search_action_circulation(
2245 { target_copy => $self->copy->id, checkin_time => undef });
2246 my $circ = $circs->[0];
2247 return unless $circ;
2248 my $u = $self->editor->retrieve_actor_user($circ->usr)
2249 or return $self->bail_on_events($self->editor->event);
2253 sub check_checkin_copy_status {
2255 my $copy = $self->copy;
2261 my $status = $U->copy_status($copy->status)->id;
2264 if( $status == OILS_COPY_STATUS_AVAILABLE ||
2265 $status == OILS_COPY_STATUS_CHECKED_OUT ||
2266 $status == OILS_COPY_STATUS_IN_PROCESS ||
2267 $status == OILS_COPY_STATUS_ON_HOLDS_SHELF ||
2268 $status == OILS_COPY_STATUS_IN_TRANSIT ||
2269 $status == OILS_COPY_STATUS_CATALOGING ||
2270 $status == OILS_COPY_STATUS_RESHELVING );
2272 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2273 if( $status == OILS_COPY_STATUS_LOST );
2275 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2276 if( $status == OILS_COPY_STATUS_MISSING );
2278 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2283 # --------------------------------------------------------------------------
2284 # On checkin, we need to return as many relevant objects as we can
2285 # --------------------------------------------------------------------------
2286 sub checkin_flesh_events {
2289 if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events}
2290 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2291 $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2295 for my $evt (@{$self->events}) {
2298 $payload->{copy} = $U->unflesh_copy($self->copy);
2299 $payload->{record} = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2300 $payload->{circ} = $self->circ;
2301 $payload->{transit} = $self->transit;
2302 $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2304 # $self->hold may or may not have been replaced with a
2305 # valid hold after processing a cancelled hold
2306 $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
2308 $evt->{payload} = $payload;
2313 my( $self, $msg ) = @_;
2314 my $bc = ($self->copy) ? $self->copy->barcode :
2317 my $usr = ($self->patron) ? $self->patron->id : "";
2318 $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2319 ", recipient=$usr, copy=$bc");
2325 $self->log_me("do_renew()");
2327 # Make sure there is an open circ to renew that is not
2328 # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2329 my $circ = $self->editor->search_action_circulation(
2330 { target_copy => $self->copy->id, stop_fines => undef } )->[0];
2333 $circ = $self->editor->search_action_circulation(
2335 target_copy => $self->copy->id,
2336 stop_fines => OILS_STOP_FINES_MAX_FINES,
2337 checkin_time => undef
2342 return $self->bail_on_events($self->editor->event) unless $circ;
2344 # A user is not allowed to renew another user's items without permission
2345 unless( $circ->usr eq $self->editor->requestor->id ) {
2346 return $self->bail_on_events($self->editor->events)
2347 unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2350 $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2351 if $circ->renewal_remaining < 1;
2353 # -----------------------------------------------------------------
2355 $self->renewal_remaining( $circ->renewal_remaining - 1 );
2358 $self->run_renew_permit;
2361 $self->do_checkin();
2362 return if $self->bail_out;
2364 unless( $self->permit_override ) {
2366 return if $self->bail_out;
2367 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2368 $self->remove_event('ITEM_NOT_CATALOGED');
2371 $self->override_events;
2372 return if $self->bail_out;
2375 $self->do_checkout();
2380 my( $self, $evt ) = @_;
2381 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2382 $logger->debug("circulator: removing event from list: $evt");
2383 my @events = @{$self->events};
2384 $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2389 my( $self, $evt ) = @_;
2390 $evt = (ref $evt) ? $evt->{textcode} : $evt;
2391 return grep { $_->{textcode} eq $evt } @{$self->events};
2396 sub run_renew_permit {
2401 if(!$self->legacy_script_support) {
2402 my $results = $self->run_indb_circ_test;
2403 unless($self->circ_test_success) {
2404 push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}}) for @$results;
2409 my $runner = $self->script_runner;
2411 $runner->load($self->circ_permit_renew);
2412 my $result = $runner->run or
2413 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2414 my $events = $result->{events};
2417 $logger->activity("ciculator: circ_permit_renew for user ".
2418 $self->patron->id." returned events: @$events") if @$events;
2420 $self->push_events(OpenILS::Event->new($_)) for @$events;
2422 $logger->debug("circulator: re-creating script runner to be safe");
2423 #$self->mk_script_runner;
2427 sub append_reading_list {
2431 $self->is_checkout and
2436 my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2438 # verify history is globally enabled and uses the bucket mechanism
2439 my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2440 apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2442 unless($htype eq 'bucket') {
2447 # verify the patron wants to retain the hisory
2448 my $setting = $e->search_actor_user_setting(
2449 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2451 unless($setting and $setting->value) {
2456 my $bkt = $e->search_container_copy_bucket(
2457 {owner => $self->patron->id, btype => 'circ_history'})->[0];
2462 # find the next item position
2463 my $last_item = $e->search_container_copy_bucket_item(
2464 {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2465 $pos = $last_item->pos + 1 if $last_item;
2468 # create the history bucket if necessary
2469 $bkt = Fieldmapper::container::copy_bucket->new;
2470 $bkt->owner($self->patron->id);
2472 $bkt->btype('reading_list');
2474 $e->create_container_copy_bucket($bkt) or return $e->die_event;
2477 my $item = Fieldmapper::container::copy_bucket_item->new;
2479 $item->bucket($bkt->id);
2480 $item->target_copy($self->copy->id);
2483 $e->create_container_copy_bucket_item($item) or return $e->die_event;
2490 sub make_trigger_events {
2492 return unless $self->circ;
2493 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2494 $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2495 $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2501 sub checkin_handle_lost_now_found {
2502 my ($self, $bill_type) = @_;
2504 # ------------------------------------------------------------------
2505 # remove charge from patron's account if lost item is returned
2506 # ------------------------------------------------------------------
2508 my $bills = $self->editor->search_money_billing(
2510 xact => $self->circ->id,
2515 $logger->debug("voiding lost item charge of ".scalar(@$bills));
2516 for my $bill (@$bills) {
2517 if( !$U->is_true($bill->voided) ) {
2518 $logger->info("lost item returned - voiding bill ".$bill->id);
2520 $bill->void_time('now');
2521 $bill->voider($self->editor->requestor->id);
2522 my $note = ($bill->note) ? $bill->note . "\n" : '';
2523 $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2525 $self->bail_on_events($self->editor->event)
2526 unless $self->editor->update_money_billing($bill);
2531 sub checkin_handle_lost_now_found_restore_od {
2534 # ------------------------------------------------------------------
2535 # restore those overdue charges voided when item was set to lost
2536 # ------------------------------------------------------------------
2538 my $ods = $self->editor->search_money_billing(
2540 xact => $self->circ->id,
2545 $logger->debug("returning overdue charges pre-lost ".scalar(@$ods));
2546 for my $bill (@$ods) {
2547 if( $U->is_true($bill->voided) ) {
2548 $logger->info("lost item returned - restoring overdue ".$bill->id);
2550 $bill->clear_void_time;
2551 $bill->voider($self->editor->requestor->id);
2552 my $note = ($bill->note) ? $bill->note . "\n" : '';
2553 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2555 $self->bail_on_events($self->editor->event)
2556 unless $self->editor->update_money_billing($bill);