]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
represent due date in local time zone (eventually org unit time zone). cleaned up...
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Circulate.pm
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";
10
11 my %scripts;
12 my $script_libs;
13 my $legacy_script_support = 0;
14
15 sub initialize {
16
17     my $self = shift;
18     my $conf = OpenSRF::Utils::SettingsClient->new;
19     my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
20
21     $legacy_script_support = $conf->config_value(@pfx2, 'legacy_script_support');
22     $legacy_script_support = ($legacy_script_support and $legacy_script_support =~ /true/i);
23
24     my $lb  = $conf->config_value(  @pfx2, 'script_path' );
25     $lb = [ $lb ] unless ref($lb);
26     $script_libs = $lb;
27
28     return unless $legacy_script_support;
29
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' );
37
38     $logger->error( "Missing circ script(s)" ) 
39         unless( $p and $c and $d and $f and $m and $pr );
40
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;
47
48     $logger->debug(
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.  ".
56         "lib paths = @$lb. ".
57         "legacy script support = ". ($legacy_script_support) ? 'yes' : 'no'
58         );
59 }
60
61
62 __PACKAGE__->register_method(
63     method  => "run_method",
64     api_name    => "open-ils.circ.checkout.permit",
65     notes       => q/
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.  
73     /);
74
75
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/,
80 );
81
82
83 __PACKAGE__->register_method(
84     method  => "run_method",
85     api_name    => "open-ils.circ.checkout",
86     notes => q/
87         Checks out an item
88         @param authtoken The login session key
89         @param params A named hash of params including:
90             copy            The copy object
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
102     /);
103
104 __PACKAGE__->register_method(
105     method  => "run_method",
106     api_name    => "open-ils.circ.checkin",
107     argc        => 2,
108     signature   => q/
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             ...
115     /
116 );
117
118 __PACKAGE__->register_method(
119     method  => "run_method",
120     api_name    => "open-ils.circ.checkin.override",
121     signature   => q/@see open-ils.circ.checkin/
122 );
123
124 __PACKAGE__->register_method(
125     method  => "run_method",
126     api_name    => "open-ils.circ.renew.override",
127     signature   => q/@see open-ils.circ.renew/,
128 );
129
130
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.
140     NOTES
141
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");
148
149 __PACKAGE__->register_method(
150     method  => "run_method",
151     api_name    => "open-ils.circ.checkout.inspect",
152     desc => q/
153         Returns the circ matrix test result and, on success, the rule set and matrix test object
154     /
155 );
156
157
158
159 sub run_method {
160     my( $self, $conn, $auth, $args ) = @_;
161     translate_legacy_args($args);
162     my $api = $self->api_name;
163
164     my $circulator = 
165         OpenILS::Application::Circ::Circulator->new($auth, %$args);
166
167     return circ_events($circulator) if $circulator->bail_out;
168
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/;
175
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});
183     } else {
184         $circulator->mk_env();
185     }
186     return circ_events($circulator) if $circulator->bail_out;
187
188     
189     $circulator->override(1) if $api =~ /override/o;
190
191     if( $api =~ /checkout\.permit/ ) {
192         $circulator->do_permit();
193
194     } elsif( $api =~ /checkout.full/ ) {
195
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();
204         }
205
206     } elsif( $api =~ /inspect/ ) {
207         my $data = $circulator->do_inspect();
208         $circulator->editor->rollback;
209         return $data;
210
211     } elsif( $api =~ /checkout/ ) {
212         $circulator->is_checkout(1);
213         $circulator->do_checkout();
214
215     } elsif( $api =~ /checkin/ ) {
216         $circulator->do_checkin();
217
218     } elsif( $api =~ /renew/ ) {
219         $circulator->is_renewal(1);
220         $circulator->do_renew();
221     }
222
223     if( $circulator->bail_out ) {
224
225         my @ee;
226         # make sure no success event accidentally slip in
227         $circulator->events(
228             [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
229
230         # Log the events
231         my @e = @{$circulator->events};
232         push( @ee, $_->{textcode} ) for @e;
233         $logger->info("circulator: bailing out with events: @ee");
234
235         $circulator->editor->rollback;
236
237     } else {
238         $circulator->editor->commit;
239     }
240
241     $circulator->script_runner->cleanup if $circulator->script_runner;
242     
243     $conn->respond_complete(circ_events($circulator));
244
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;
251     }
252 }
253
254 sub circ_events {
255     my $circ = shift;
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;
260 }
261
262
263 sub translate_legacy_args {
264     my $args = shift;
265
266     if( $$args{barcode} ) {
267         $$args{copy_barcode} = $$args{barcode};
268         delete $$args{barcode};
269     }
270
271     if( $$args{copyid} ) {
272         $$args{copy_id} = $$args{copyid};
273         delete $$args{copyid};
274     }
275
276     if( $$args{patronid} ) {
277         $$args{patron_id} = $$args{patronid};
278         delete $$args{patronid};
279     }
280
281     if( $$args{patron} and !ref($$args{patron}) ) {
282         $$args{patron_id} = $$args{patron};
283         delete $$args{patron};
284     }
285
286
287     if( $$args{noncat} ) {
288         $$args{is_noncat} = $$args{noncat};
289         delete $$args{noncat};
290     }
291
292     if( $$args{precat} ) {
293         $$args{is_precat} = $$args{request_precat} = $$args{precat};
294         delete $$args{precat};
295     }
296 }
297
298
299
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/;
306 use DateTime;
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;
322 use Time::Local;
323
324 my $holdcode    = "OpenILS::Application::Circ::Holds";
325 my $transcode   = "OpenILS::Application::Circ::Transit";
326 my %user_groups;
327
328 sub DESTROY { }
329
330
331 # --------------------------------------------------------------------------
332 # Add a pile of automagic getter/setter methods
333 # --------------------------------------------------------------------------
334 my @AUTOLOAD_FIELDS = qw/
335     notify_hold
336     remote_hold
337     backdate
338     copy
339     copy_id
340     copy_barcode
341     patron
342     patron_id
343     patron_barcode
344     script_runner
345     volume
346     title
347     is_renewal
348     is_checkout
349     is_noncat
350     is_precat
351     request_precat
352     is_checkin
353     noncat_type
354     editor
355     events
356     cache_handle
357     override
358     circ_permit_patron
359     circ_permit_copy
360     circ_duration
361     circ_recurring_fines
362     circ_max_fines
363     circ_permit_renew
364     circ
365     transit
366     hold
367     permit_key
368     noncat_circ_lib
369     noncat_count
370     checkout_time
371     dummy_title
372     dummy_author
373     dummy_isbn
374     circ_modifier
375     circ_lib
376     barcode
377     duration_level
378     recurring_fines_level
379     duration_rule
380     recurring_fines_rule
381     max_fine_rule
382     renewal_remaining
383     due_date
384     fulfilled_holds
385     transit
386     checkin_changed
387     force
388     old_circ
389     permit_override
390     pending_checkouts
391     cancelled_hold_transit
392     opac_renewal
393     phone_renewal
394     desk_renewal
395     retarget
396     matrix_test_result
397     circ_matrix_matchpoint
398     circ_test_success
399     legacy_script_support
400     is_deposit
401     is_rental
402     deposit_billing
403     rental_billing
404     capture
405     noop
406 /;
407
408
409 sub AUTOLOAD {
410     my $self = shift;
411     my $type = ref($self) or die "$self is not an object";
412     my $data = shift;
413     my $name = $AUTOLOAD;
414     $name =~ s/.*://o;   
415
416     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
417         $logger->error("circulator: $type: invalid autoload field: $name");
418         die "$type: invalid autoload field: $name\n" 
419     }
420
421     {
422         no strict 'refs';
423         *{"${type}::${name}"} = sub {
424             my $s = shift;
425             my $v = shift;
426             $s->{$name} = $v if defined $v;
427             return $s->{$name};
428         }
429     }
430     return $self->$name($data);
431 }
432
433
434 sub new {
435     my( $class, $auth, %args ) = @_;
436     $class = ref($class) || $class;
437     my $self = bless( {}, $class );
438
439     $self->events([]);
440     $self->editor(new_editor(xact => 1, authtoken => $auth));
441
442     unless( $self->editor->checkauth ) {
443         $self->bail_on_events($self->editor->event);
444         return $self;
445     }
446
447     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
448
449     $self->$_($args{$_}) for keys %args;
450
451     $self->circ_lib(
452         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
453
454     # if this is a renewal, default to desk_renewal
455     $self->desk_renewal(1) unless 
456         $self->opac_renewal or $self->phone_renewal;
457
458     $self->capture('') unless $self->capture;
459
460     unless(%user_groups) {
461         my $gps = $self->editor->retrieve_all_permission_grp_tree;
462         %user_groups = map { $_->id => $_ } @$gps;
463     }
464
465     return $self;
466 }
467
468
469 # --------------------------------------------------------------------------
470 # True if we should discontinue processing
471 # --------------------------------------------------------------------------
472 sub bail_out {
473     my( $self, $bool ) = @_;
474     if( defined $bool ) {
475         $logger->info("circulator: BAILING OUT") if $bool;
476         $self->{bail_out} = $bool;
477     }
478     return $self->{bail_out};
479 }
480
481
482 sub push_events {
483     my( $self, @evts ) = @_;
484     for my $e (@evts) {
485         next unless $e;
486         $logger->info("circulator: pushing event ".$e->{textcode});
487         push( @{$self->events}, $e ) unless
488             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
489     }
490 }
491
492 sub mk_permit_key {
493     my $self = shift;
494     my $key = md5_hex( time() . rand() . "$$" );
495     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
496     return $self->permit_key($key);
497 }
498
499 sub check_permit_key {
500     my $self = shift;
501     my $key = $self->permit_key;
502     return 0 unless $key;
503     my $k = "oils_permit_key_$key";
504     my $one = $self->cache_handle->get_cache($k);
505     $self->cache_handle->delete_cache($k);
506     return ($one) ? 1 : 0;
507 }
508
509 sub mk_env {
510     my $self = shift;
511     my $e = $self->editor;
512
513     # --------------------------------------------------------------------------
514     # Grab the fleshed copy
515     # --------------------------------------------------------------------------
516     unless($self->is_noncat) {
517         my $copy;
518             my $flesh = { 
519                     flesh => 2, 
520                     flesh_fields => {acp => ['call_number'], acn => ['record']} 
521             };
522             if($self->copy_id) {
523                     $copy = $e->retrieve_asset_copy(
524                             [$self->copy_id, $flesh ]) or return $e->event;
525     
526             } elsif( $self->copy_barcode ) {
527     
528                     $copy = $e->search_asset_copy(
529                             [{barcode => $self->copy_barcode, deleted => 'f'}, $flesh ])->[0];
530             }
531     
532         if($copy) {
533             $self->copy($copy);
534             $self->volume($copy->call_number);
535             $self->title($self->volume->record);
536             $self->copy->call_number($self->volume->id);
537             $self->volume->record($self->title->id);
538             $self->is_precat(1) if $self->volume->id == OILS_PRECAT_CALL_NUMBER;
539             if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
540                 $self->is_deposit(1) if $U->is_true($self->copy->deposit);
541                 $self->is_rental(1) unless $U->is_true($self->copy->deposit);
542             }
543         } else {
544             # We can't renew if there is no copy
545             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
546                 if $self->is_renewal;
547             $self->is_precat(1);
548         }
549     }
550
551     # --------------------------------------------------------------------------
552     # Grab the patron
553     # --------------------------------------------------------------------------
554     my $patron;
555         if( $self->patron_id ) {
556                 $patron = $e->retrieve_actor_user($self->patron_id) or return $e->event;
557
558         } elsif( $self->patron_barcode ) {
559
560                 my $card = $e->search_actor_card( 
561                         {barcode => $self->patron_barcode})->[0] or return $e->event;
562
563                 $patron = $e->search_actor_user( 
564                         {card => $card->id})->[0] or return $e->event;
565
566         } else {
567                 if( my $copy = $self->copy ) {
568                         my $circs = $e->search_action_circulation(
569                                 {target_copy => $copy->id, checkin_time => undef});
570
571                         if( my $circ = $circs->[0] ) {
572                                 $patron = $e->retrieve_actor_user($circ->usr)
573                                         or return $e->event;
574                         }
575                 }
576         }
577
578     return $self->bail_on_events(OpenILS::Event->new('ACTOR_USER_NOT_FOUND'))
579         unless $self->patron($patron) or $self->is_checkin;
580 }
581
582 # --------------------------------------------------------------------------
583 # This builds the script runner environment and fetches most of the
584 # objects we need
585 # --------------------------------------------------------------------------
586 sub mk_script_runner {
587     my $self = shift;
588     my $args = {};
589
590
591     my @fields = 
592         qw/copy copy_barcode copy_id patron 
593             patron_id patron_barcode volume title editor/;
594
595     # Translate our objects into the ScriptBuilder args hash
596     $$args{$_} = $self->$_() for @fields;
597
598     $args->{ignore_user_status} = 1 if $self->is_checkin;
599     $$args{fetch_patron_by_circ_copy} = 1;
600     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
601
602     if( my $pco = $self->pending_checkouts ) {
603         $logger->info("circulator: we were given a pending checkouts number of $pco");
604         $$args{patronItemsOut} = $pco;
605     }
606
607     # This fetches most of the objects we need
608     $self->script_runner(
609         OpenILS::Application::Circ::ScriptBuilder->build($args));
610
611     # Now we translate the ScriptBuilder objects back into self
612     $self->$_($$args{$_}) for @fields;
613
614     my @evts = @{$args->{_events}} if $args->{_events};
615
616     $logger->debug("circulator: script builder returned events: @evts") if @evts;
617
618
619     if(@evts) {
620         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
621         if(!$self->is_noncat and 
622             @evts == 1 and 
623             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
624                 $self->is_precat(1);
625
626         } else {
627             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
628             return $self->bail_on_events(@e);
629         }
630     }
631
632     if($self->copy) {
633         $self->is_precat(1) if $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
634         if($self->copy->deposit_amount and $self->copy->deposit_amount > 0) {
635             $self->is_deposit(1) if $U->is_true($self->copy->deposit);
636             $self->is_rental(1) unless $U->is_true($self->copy->deposit);
637         }
638     }
639
640     # We can't renew if there is no copy
641     return $self->bail_on_events(@evts) if 
642         $self->is_renewal and !$self->copy;
643
644     # Set some circ-specific flags in the script environment
645     my $evt = "environment";
646     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
647
648     if( $self->is_noncat ) {
649       $self->script_runner->insert("$evt.isNonCat", 1);
650       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
651     }
652
653     if( $self->is_precat ) {
654         $self->script_runner->insert("environment.isPrecat", 1, 1);
655     }
656
657     $self->script_runner->add_path( $_ ) for @$script_libs;
658
659     return 1;
660 }
661
662 # --------------------------------------------------------------------------
663 # Does the circ permit work
664 # --------------------------------------------------------------------------
665 sub do_permit {
666     my $self = shift;
667
668     $self->log_me("do_permit()");
669
670     unless( $self->editor->requestor->id == $self->patron->id ) {
671         return $self->bail_on_events($self->editor->event)
672             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
673     }
674
675     $self->check_captured_holds();
676     $self->do_copy_checks();
677     return if $self->bail_out;
678     $self->run_patron_permit_scripts();
679     $self->run_copy_permit_scripts() 
680         unless $self->is_precat or $self->is_noncat;
681     $self->check_item_deposit_events();
682     $self->override_events();
683     return if $self->bail_out;
684
685     if($self->is_precat and not $self->request_precat) {
686         $self->push_events(
687             OpenILS::Event->new(
688                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
689         return $self->bail_out(1) unless $self->is_renewal;
690     }
691
692     $self->push_events(
693         OpenILS::Event->new('SUCCESS', payload => $self->mk_permit_key));
694 }
695
696 sub check_item_deposit_events {
697     my $self = shift;
698     $self->push_events(OpenILS::Event->new('ITEM_DEPOSIT_REQUIRED', payload => $self->copy)) 
699         if $self->is_deposit and not $self->is_deposit_exempt;
700     $self->push_events(OpenILS::Event->new('ITEM_RENTAL_FEE_REQUIRED', payload => $self->copy)) 
701         if $self->is_rental and not $self->is_rental_exempt;
702 }
703
704 # returns true if the user is not required to pay deposits
705 sub is_deposit_exempt {
706     my $self = shift;
707     my $pid = (ref $self->patron->profile) ?
708         $self->patron->profile->id : $self->patron->profile;
709     my $groups = $U->ou_ancestor_setting_value(
710         $self->circ_lib, 'circ.deposit.exempt_groups', $self->editor);
711     for my $grp (@$groups) {
712         return 1 if $self->is_group_descendant($grp, $pid);
713     }
714     return 0;
715 }
716
717 # returns true if the user is not required to pay rental fees
718 sub is_rental_exempt {
719     my $self = shift;
720     my $pid = (ref $self->patron->profile) ?
721         $self->patron->profile->id : $self->patron->profile;
722     my $groups = $U->ou_ancestor_setting_value(
723         $self->circ_lib, 'circ.rental.exempt_groups', $self->editor);
724     for my $grp (@$groups) {
725         return 1 if $self->is_group_descendant($grp, $pid);
726     }
727     return 0;
728 }
729
730 sub is_group_descendant {
731     my($self, $p_id, $c_id) = @_;
732     return 0 unless defined $p_id and defined $c_id;
733     return 1 if $c_id == $p_id;
734     while(my $grp = $user_groups{$c_id}) {
735         $c_id = $grp->parent;
736         return 0 unless defined $c_id;
737         return 1 if $c_id == $p_id;
738     }
739     return 0;
740 }
741
742 sub check_captured_holds {
743    my $self    = shift;
744    my $copy    = $self->copy;
745    my $patron  = $self->patron;
746
747     return undef unless $copy;
748
749     my $s = $U->copy_status($copy->status)->id;
750     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
751     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
752
753     # Item is on the holds shelf, make sure it's going to the right person
754     my $holds   = $self->editor->search_action_hold_request(
755         [
756             { 
757                 current_copy        => $copy->id , 
758                 capture_time        => { '!=' => undef },
759                 cancel_time         => undef, 
760                 fulfillment_time    => undef 
761             },
762             { limit => 1 }
763         ]
764     );
765
766     if( $holds and $$holds[0] ) {
767         return undef if $$holds[0]->usr == $patron->id;
768     }
769
770     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
771
772     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
773 }
774
775
776 sub do_copy_checks {
777     my $self = shift;
778     my $copy = $self->copy;
779     return unless $copy;
780
781     my $stat = $U->copy_status($copy->status)->id;
782
783     # We cannot check out a copy if it is in-transit
784     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
785         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
786     }
787
788     $self->handle_claims_returned();
789     return if $self->bail_out;
790
791     # no claims returned circ was found, check if there is any open circ
792     unless( $self->is_renewal ) {
793
794         my $circs = $self->editor->search_action_circulation(
795             { target_copy => $copy->id, checkin_time => undef }
796         );
797
798         if(my $old_circ = $circs->[0]) { # an open circ was found
799
800             my $payload; # event payload
801
802             if($old_circ->usr == $self->patron->id) {
803                 
804                 $payload = {old_circ => $old_circ};
805
806                 # If there is an open circulation on the checkout item and an auto-renew 
807                 # interval is defined, inform the caller that they should go 
808                 # ahead and renew the item instead of warning about open circulations.
809     
810                 my $auto_renew_intvl = $U->ou_ancestor_setting_value(        
811                     $self->editor->requestor->ws_ou, 
812                     'circ.checkout_auto_renew_age', 
813                     $self->editor
814                 );
815
816                 if($auto_renew_intvl) {
817                     my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
818                     my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($old_circ->xact_start) );
819
820                     if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
821                         $payload->{auto_renew} = 1;
822                     }
823                 }
824             }
825
826             return $self->bail_on_events(
827                 OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $payload)
828             );
829         }
830     }
831 }
832
833 my $LEGACY_CIRC_EVENT_MAP = {
834     'actor.usr.barred' => 'PATRON_BARRED',
835     'asset.copy.circulate' =>  'COPY_CIRC_NOT_ALLOWED',
836     'asset.copy.status' => 'COPY_NOT_AVAILABLE',
837     'asset.copy_location.circulate' => 'COPY_CIRC_NOT_ALLOWED',
838     'config.circ_matrix_test.circulate' => 'COPY_CIRC_NOT_ALLOWED',
839     'config.circ_matrix_test.max_items_out' =>  'PATRON_EXCEEDS_CHECKOUT_COUNT',
840     'config.circ_matrix_test.max_overdue' =>  'PATRON_EXCEEDS_OVERDUE_COUNT',
841     'config.circ_matrix_test.max_fines' => 'PATRON_EXCEEDS_FINES',
842     'config.circ_matrix_circ_mod_test' => 'PATRON_EXCEEDS_CHECKOUT_COUNT',
843 };
844
845
846 # ---------------------------------------------------------------------
847 # This pushes any patron-related events into the list but does not
848 # set bail_out for any events
849 # ---------------------------------------------------------------------
850 sub run_patron_permit_scripts {
851     my $self        = shift;
852     my $runner      = $self->script_runner;
853     my $patronid    = $self->patron->id;
854
855     my @allevents; 
856
857     if(!$self->legacy_script_support) {
858
859         my $results = $self->run_indb_circ_test;
860         unless($self->circ_test_success) {
861             push(@allevents, OpenILS::Event->new(
862                 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
863                 )) for @$results;
864         }
865
866     } else {
867
868         # --------------------------------------------------------------------- 
869         # # Now run the patron permit script 
870         # ---------------------------------------------------------------------
871         $runner->load($self->circ_permit_patron);
872         my $result = $runner->run or 
873             throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
874
875         my $patron_events = $result->{events};
876
877         OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
878         my $mask = ($self->is_renewal) ? 'RENEW' : 'CIRC';
879         my $penalties = OpenILS::Utils::Penalty->retrieve_penalties($self->editor, $patronid, $self->circ_lib, $mask);
880         $penalties = $penalties->{fatal_penalties};
881
882         for my $pen (@$penalties) {
883             my $event = OpenILS::Event->new($pen->name);
884             $event->{desc} = $pen->label;
885             push(@allevents, $event);
886         }
887
888         push(@allevents, OpenILS::Event->new($_)) for (@$patron_events);
889     }
890
891     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
892
893     $self->push_events(@allevents);
894 }
895
896 sub run_indb_circ_test {
897     my $self = shift;
898     return $self->matrix_test_result if $self->matrix_test_result;
899
900     my $dbfunc = ($self->is_renewal) ? 
901         'action.item_user_renew_test' : 'action.item_user_circ_test';
902
903     my $results = $self->editor->json_query(
904         {   from => [
905                 $dbfunc,
906                 $self->editor->requestor->ws_ou,
907                 ($self->is_precat or $self->is_noncat) ? undef : $self->copy->id, 
908                 $self->patron->id,
909             ]
910         }
911     );
912
913     $self->circ_test_success($U->is_true($results->[0]->{success}));
914
915     if(my $mp = $results->[0]->{matchpoint}) {
916         $self->circ_matrix_matchpoint(
917             $self->editor->retrieve_config_circ_matrix_matchpoint([
918                 $mp,
919                 {   flesh => 1,
920                     flesh_fields => {ccmm => 
921                         ['duration_rule', 'recurring_fine_rule', 'max_fine_rule']}
922                 }
923             ])
924         );
925     }
926
927     return $self->matrix_test_result($results);
928 }
929
930 # ---------------------------------------------------------------------
931 # given a use and copy, this will calculate the circulation policy
932 # parameters.  Only works with in-db circ.
933 # ---------------------------------------------------------------------
934 sub do_inspect {
935     my $self = shift;
936
937     return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $self->copy;
938
939     $self->run_indb_circ_test;
940
941     my $results = {
942         circ_test_success => $self->circ_test_success,
943         failure_events => [],
944         failure_codes => [],
945     };
946
947     unless($self->circ_test_success) {
948         push(@{$results->{failure_codes}}, 
949             $_->{fail_part}) for @{$self->matrix_test_result};
950         push(@{$results->{failure_events}}, 
951             $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) 
952                 for @{$self->matrix_test_result};
953     }
954
955     if($self->circ_matrix_matchpoint) {
956         my $duration_rule = $self->circ_matrix_matchpoint->duration_rule;
957         my $recurring_fine_rule = $self->circ_matrix_matchpoint->recurring_fine_rule;
958         my $max_fine_rule = $self->circ_matrix_matchpoint->max_fine_rule;
959     
960         my $policy = $self->get_circ_policy(
961             $duration_rule, $recurring_fine_rule, $max_fine_rule);
962     
963         $$results{$_} = $$policy{$_} for keys %$policy;
964     }
965
966     return $results;
967 }
968
969 # ---------------------------------------------------------------------
970 # Loads the circ policy info for duration, recurring fine, and max
971 # fine based on the current copy
972 # ---------------------------------------------------------------------
973 sub get_circ_policy {
974     my($self, $duration_rule, $recurring_fine_rule, $max_fine_rule) = @_;
975
976     my $policy = {
977         duration_rule => $duration_rule->name,
978         recurring_fine_rule => $recurring_fine_rule->name,
979         max_fine_rule => $max_fine_rule->name,
980         max_fine => $self->get_max_fine_amount($max_fine_rule),
981         fine_interval => $recurring_fine_rule->recurance_interval,
982         renewal_remaining => $duration_rule->max_renewals
983     };
984
985     $policy->{duration} = $duration_rule->shrt
986         if $self->copy->loan_duration == OILS_CIRC_DURATION_SHORT;
987     $policy->{duration} = $duration_rule->normal
988         if $self->copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
989     $policy->{duration} = $duration_rule->extended
990         if $self->copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
991
992     $policy->{recurring_fine} = $recurring_fine_rule->low
993         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
994     $policy->{recurring_fine} = $recurring_fine_rule->normal
995         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
996     $policy->{recurring_fine} = $recurring_fine_rule->high
997         if $self->copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
998
999     return $policy;
1000 }
1001
1002 sub get_max_fine_amount {
1003     my $self = shift;
1004     my $max_fine_rule = shift;
1005     my $max_amount = $max_fine_rule->amount;
1006
1007     # if is_percent is true then the max->amount is
1008     # use as a percentage of the copy price
1009     if ($U->is_true($max_fine_rule->is_percent)) {
1010         my $price = $U->get_copy_price($self->editor, $self->copy, $self->volume);
1011         $max_amount = $price * $max_fine_rule->amount / 100;
1012     }  
1013
1014     return $max_amount;
1015 }
1016
1017
1018
1019 sub run_copy_permit_scripts {
1020     my $self = shift;
1021     my $copy = $self->copy || return;
1022     my $runner = $self->script_runner;
1023
1024     my @allevents;
1025
1026     if(!$self->legacy_script_support) {
1027         my $results = $self->run_indb_circ_test;
1028         unless($self->circ_test_success) {
1029             push(@allevents, OpenILS::Event->new(
1030                 $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}
1031                 )) for @$results;
1032         }
1033     } else {
1034     
1035        # ---------------------------------------------------------------------
1036        # Capture all of the copy permit events
1037        # ---------------------------------------------------------------------
1038        $runner->load($self->circ_permit_copy);
1039        my $result = $runner->run or 
1040             throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
1041        my $copy_events = $result->{events};
1042
1043        # ---------------------------------------------------------------------
1044        # Now collect all of the events together
1045        # ---------------------------------------------------------------------
1046        push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
1047     }
1048
1049     # See if this copy has an alert message
1050     my $ae = $self->check_copy_alert();
1051     push( @allevents, $ae ) if $ae;
1052
1053    # uniquify the events
1054    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
1055    @allevents = values %hash;
1056
1057    for (@allevents) {
1058       $_->{payload} = $copy if 
1059             ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
1060    }
1061
1062     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
1063
1064     $self->push_events(@allevents);
1065 }
1066
1067
1068 sub check_copy_alert {
1069     my $self = shift;
1070     return undef if $self->is_renewal;
1071     return OpenILS::Event->new(
1072         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
1073         if $self->copy and $self->copy->alert_message;
1074     return undef;
1075 }
1076
1077
1078
1079 # --------------------------------------------------------------------------
1080 # If the call is overriding and has permissions to override every collected
1081 # event, the are cleared.  Any event that the caller does not have
1082 # permission to override, will be left in the event list and bail_out will
1083 # be set
1084 # XXX We need code in here to cancel any holds/transits on copies 
1085 # that are being force-checked out
1086 # --------------------------------------------------------------------------
1087 sub override_events {
1088     my $self = shift;
1089     my @events = @{$self->events};
1090     return unless @events;
1091
1092     if(!$self->override) {
1093         return $self->bail_out(1) 
1094             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
1095     }   
1096
1097     $self->events([]);
1098     
1099    for my $e (@events) {
1100       my $tc = $e->{textcode};
1101       next if $tc eq 'SUCCESS';
1102       my $ov = "$tc.override";
1103       $logger->info("circulator: attempting to override event: $ov");
1104
1105         return $self->bail_on_events($self->editor->event)
1106             unless( $self->editor->allowed($ov) );
1107    }
1108 }
1109     
1110
1111 # --------------------------------------------------------------------------
1112 # If there is an open claimsreturn circ on the requested copy, close the 
1113 # circ if overriding, otherwise bail out
1114 # --------------------------------------------------------------------------
1115 sub handle_claims_returned {
1116     my $self = shift;
1117     my $copy = $self->copy;
1118
1119     my $CR = $self->editor->search_action_circulation(
1120         {   
1121             target_copy     => $copy->id,
1122             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
1123             checkin_time    => undef,
1124         }
1125     );
1126
1127     return unless ($CR = $CR->[0]); 
1128
1129     my $evt;
1130
1131     # - If the caller has set the override flag, we will check the item in
1132     if($self->override) {
1133
1134         $CR->checkin_time('now');   
1135         $CR->checkin_scan_time('now');   
1136         $CR->checkin_lib($self->editor->requestor->ws_ou);
1137         $CR->checkin_workstation($self->editor->requestor->wsid);
1138         $CR->checkin_staff($self->editor->requestor->id);
1139
1140         $evt = $self->editor->event 
1141             unless $self->editor->update_action_circulation($CR);
1142
1143     } else {
1144         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1145     }
1146
1147     $self->bail_on_events($evt) if $evt;
1148     return;
1149 }
1150
1151
1152 # --------------------------------------------------------------------------
1153 # This performs the checkout
1154 # --------------------------------------------------------------------------
1155 sub do_checkout {
1156     my $self = shift;
1157
1158     $self->log_me("do_checkout()");
1159
1160     # make sure perms are good if this isn't a renewal
1161     unless( $self->is_renewal ) {
1162         return $self->bail_on_events($self->editor->event)
1163             unless( $self->editor->allowed('COPY_CHECKOUT') );
1164     }
1165
1166     # verify the permit key
1167     unless( $self->check_permit_key ) {
1168         if( $self->permit_override ) {
1169             return $self->bail_on_events($self->editor->event)
1170                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
1171         } else {
1172             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
1173         }   
1174     }
1175
1176     # if this is a non-cataloged circ, build the circ and finish
1177     if( $self->is_noncat ) {
1178         $self->checkout_noncat;
1179         $self->push_events(
1180             OpenILS::Event->new('SUCCESS', 
1181             payload => { noncat_circ => $self->circ }));
1182         return;
1183     }
1184
1185     if( $self->is_precat ) {
1186         $self->make_precat_copy;
1187         return if $self->bail_out;
1188
1189     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
1190         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1191     }
1192
1193     $self->do_copy_checks;
1194     return if $self->bail_out;
1195
1196     $self->run_checkout_scripts();
1197     return if $self->bail_out;
1198
1199     $self->build_checkout_circ_object();
1200     return if $self->bail_out;
1201
1202     $self->apply_modified_due_date();
1203     return if $self->bail_out;
1204
1205     return $self->bail_on_events($self->editor->event)
1206         unless $self->editor->create_action_circulation($self->circ);
1207
1208     # refresh the circ to force local time zone for now
1209     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
1210
1211     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
1212     $self->update_copy;
1213     return if $self->bail_out;
1214
1215     $self->apply_deposit_fee();
1216     return if $self->bail_out;
1217
1218     $self->handle_checkout_holds();
1219     return if $self->bail_out;
1220
1221     # ------------------------------------------------------------------------------
1222     # Update the patron penalty info in the DB.  Run it for permit-overrides 
1223     # since the penalties are not updated during the permit phase
1224     # ------------------------------------------------------------------------------
1225     OpenILS::Utils::Penalty->calculate_penalties($self->editor, $self->patron->id, $self->circ_lib);
1226
1227     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
1228     $self->push_events(
1229         OpenILS::Event->new('SUCCESS',
1230             payload  => {
1231                 copy              => $U->unflesh_copy($self->copy),
1232                 circ              => $self->circ,
1233                 record            => $record,
1234                 holds_fulfilled   => $self->fulfilled_holds,
1235                 deposit_billing      => $self->deposit_billing,
1236                 rental_billing       => $self->rental_billing
1237             }
1238         )
1239     );
1240 }
1241
1242 sub apply_deposit_fee {
1243     my $self = shift;
1244     my $copy = $self->copy;
1245     return unless 
1246         ($self->is_deposit and not $self->is_deposit_exempt) or 
1247         ($self->is_rental and not $self->is_rental_exempt);
1248
1249         my $bill = Fieldmapper::money::billing->new;
1250     my $amount = $copy->deposit_amount;
1251     my $billing_type;
1252     my $btype;
1253
1254     if($self->is_deposit) {
1255         $billing_type = OILS_BILLING_TYPE_DEPOSIT;
1256         $btype = 5;
1257         $self->deposit_billing($bill);
1258     } else {
1259         $billing_type = OILS_BILLING_TYPE_RENTAL;
1260         $btype = 6;
1261         $self->rental_billing($bill);
1262     }
1263
1264         $bill->xact($self->circ->id);
1265         $bill->amount($amount);
1266         $bill->note(OILS_BILLING_NOTE_SYSTEM);
1267         $bill->billing_type($billing_type);
1268         $bill->btype($btype);
1269     $self->editor->create_money_billing($bill) or $self->bail_on_events($self->editor->event);
1270
1271         $logger->info("circulator: charged $amount on checkout with billing type $billing_type");
1272 }
1273
1274 sub update_copy {
1275     my $self = shift;
1276     my $copy = $self->copy;
1277
1278     my $stat = $copy->status if ref $copy->status;
1279     my $loc = $copy->location if ref $copy->location;
1280     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
1281
1282     $copy->status($stat->id) if $stat;
1283     $copy->location($loc->id) if $loc;
1284     $copy->circ_lib($circ_lib->id) if $circ_lib;
1285     $copy->editor($self->editor->requestor->id);
1286     $copy->edit_date('now');
1287     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
1288
1289     return $self->bail_on_events($self->editor->event)
1290         unless $self->editor->update_asset_copy($self->copy);
1291
1292     $copy->status($U->copy_status($copy->status));
1293     $copy->location($loc) if $loc;
1294     $copy->circ_lib($circ_lib) if $circ_lib;
1295 }
1296
1297
1298 sub bail_on_events {
1299     my( $self, @evts ) = @_;
1300     $self->push_events(@evts);
1301     $self->bail_out(1);
1302 }
1303
1304
1305 # ------------------------------------------------------------------------------
1306 # When an item is checked out, see if we can fulfill a hold for this patron
1307 # ------------------------------------------------------------------------------
1308 sub handle_checkout_holds {
1309    my $self    = shift;
1310    my $copy    = $self->copy;
1311    my $patron  = $self->patron;
1312
1313    my $e = $self->editor;
1314    $self->fulfilled_holds([]);
1315
1316    # pre/non-cats can't fulfill a hold
1317    return if $self->is_precat or $self->is_noncat;
1318
1319     my $hold = $e->search_action_hold_request({   
1320         current_copy        => $copy->id , 
1321         cancel_time         => undef, 
1322         fulfillment_time    => undef,
1323         '-or' => [
1324             {expire_time => undef},
1325             {expire_time => {'>' => 'now'}}
1326         ]
1327     })->[0];
1328
1329     if($hold and $hold->usr != $patron->id) {
1330         # reset the hold since the copy is now checked out
1331     
1332         $logger->info("circulator: un-targeting hold ".$hold->id.
1333             " because copy ".$copy->id." is getting checked out");
1334
1335         $hold->clear_prev_check_time; 
1336         $hold->clear_current_copy;
1337         $hold->clear_capture_time;
1338
1339         return $self->bail_on_event($e->event)
1340             unless $e->update_action_hold_request($hold);
1341
1342         $hold = undef;
1343     }
1344
1345     unless($hold) {
1346         $hold = $self->find_related_user_hold($copy, $patron) or return;
1347         $logger->info("circulator: found related hold to fulfill in checkout");
1348     }
1349
1350     $logger->debug("circulator: checkout fulfilling hold " . $hold->id);
1351
1352     # if the hold was never officially captured, capture it.
1353     $hold->current_copy($copy->id);
1354     $hold->capture_time('now') unless $hold->capture_time;
1355     $hold->fulfillment_time('now');
1356     $hold->fulfillment_staff($e->requestor->id);
1357     $hold->fulfillment_lib($e->requestor->ws_ou);
1358
1359     return $self->bail_on_events($e->event)
1360         unless $e->update_action_hold_request($hold);
1361
1362     $holdcode->delete_hold_copy_maps($e, $hold->id);
1363     return $self->fulfilled_holds([$hold->id]);
1364 }
1365
1366
1367 # ------------------------------------------------------------------------------
1368 # If the circ.checkout_fill_related_hold setting is turned on and no hold for
1369 # the patron directly targets the checked out item, see if there is another hold 
1370 # (with hold_type T or V) for the patron that could be fulfilled by the checked 
1371 # out item.  Fulfill the oldest hold and only fulfill 1 of them.
1372 # ------------------------------------------------------------------------------
1373 sub find_related_user_hold {
1374     my($self, $copy, $patron) = @_;
1375     my $e = $self->editor;
1376
1377     return undef if $self->volume->id == OILS_PRECAT_CALL_NUMBER; 
1378
1379     return undef unless $U->ou_ancestor_setting_value(        
1380         $e->requestor->ws_ou, 'circ.checkout_fills_related_hold', $e);
1381
1382     # find the oldest unfulfilled hold that has not yet hit the holds shelf.
1383     my $args = {
1384         select => {ahr => ['id']}, 
1385         from => {
1386             ahr => {
1387                 acp => {
1388                     field => 'id', 
1389                     fkey => 'current_copy',
1390                     type => 'left' # there may be no current_copy
1391                 }
1392             }
1393         }, 
1394         where => {
1395             '+ahr' => {
1396                 usr => $patron->id,
1397                 fulfillment_time => undef,
1398                 cancel_time => undef,
1399                '-or' => [
1400                     {expire_time => undef},
1401                     {expire_time => {'>' => 'now'}}
1402                 ]
1403             },
1404             '-or' => [
1405                 {
1406                     '+ahr' => { 
1407                         hold_type => 'V',
1408                         target => $self->volume->id
1409                     }
1410                 },
1411                 { 
1412                     '+ahr' => { 
1413                         hold_type => 'T',
1414                         target => $self->title->id
1415                     }
1416                 },
1417             ],
1418             '+acp' => {
1419                 '-or' => [
1420                     {id => undef}, # left-join copy may be nonexistent
1421                     {status => {'!=' => OILS_COPY_STATUS_ON_HOLDS_SHELF}},
1422                 ]
1423             }
1424         },
1425         order_by => {ahr => {request_time => {direction => 'asc'}}},
1426         limit => 1
1427     };
1428
1429     my $hold_info = $e->json_query($args)->[0];
1430     return $e->retrieve_action_hold_request($hold_info->{id}) if $hold_info;
1431     return undef;
1432 }
1433
1434
1435 sub run_checkout_scripts {
1436     my $self = shift;
1437
1438     my $evt;
1439     my $runner = $self->script_runner;
1440
1441     my $duration;
1442     my $recurring;
1443     my $max_fine;
1444     my $duration_name;
1445     my $recurring_name;
1446     my $max_fine_name;
1447
1448     if(!$self->legacy_script_support) {
1449         $self->run_indb_circ_test();
1450         $duration = $self->circ_matrix_matchpoint->duration_rule;
1451         $recurring = $self->circ_matrix_matchpoint->recurring_fine_rule;
1452         $max_fine = $self->circ_matrix_matchpoint->max_fine_rule;
1453
1454     } else {
1455
1456        $runner->load($self->circ_duration);
1457
1458        my $result = $runner->run or 
1459             throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1460
1461        $duration_name   = $result->{durationRule};
1462        $recurring_name  = $result->{recurringFinesRule};
1463        $max_fine_name   = $result->{maxFine};
1464     }
1465
1466     $duration_name = $duration->name if $duration;
1467     if( $duration_name ne OILS_UNLIMITED_CIRC_DURATION ) {
1468
1469         unless($duration) {
1470             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
1471             return $self->bail_on_events($evt) if $evt;
1472         
1473             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
1474             return $self->bail_on_events($evt) if $evt;
1475         
1476             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
1477             return $self->bail_on_events($evt) if $evt;
1478         }
1479
1480     } else {
1481
1482         # The item circulates with an unlimited duration
1483         $duration   = undef;
1484         $recurring  = undef;
1485         $max_fine   = undef;
1486     }
1487
1488    $self->duration_rule($duration);
1489    $self->recurring_fines_rule($recurring);
1490    $self->max_fine_rule($max_fine);
1491 }
1492
1493
1494 sub build_checkout_circ_object {
1495     my $self = shift;
1496
1497    my $circ       = Fieldmapper::action::circulation->new;
1498    my $duration   = $self->duration_rule;
1499    my $max        = $self->max_fine_rule;
1500    my $recurring  = $self->recurring_fines_rule;
1501    my $copy       = $self->copy;
1502    my $patron     = $self->patron;
1503
1504     if( $duration ) {
1505
1506         my $policy = $self->get_circ_policy($duration, $recurring, $max);
1507
1508         my $dname = $duration->name;
1509         my $mname = $max->name;
1510         my $rname = $recurring->name;
1511
1512         $logger->debug("circulator: building circulation ".
1513             "with duration=$dname, maxfine=$mname, recurring=$rname");
1514     
1515         $circ->duration($policy->{duration});
1516         $circ->recuring_fine($policy->{recurring_fine});
1517         $circ->duration_rule($duration->name);
1518         $circ->recuring_fine_rule($recurring->name);
1519         $circ->max_fine_rule($max->name);
1520         $circ->max_fine($policy->{max_fine});
1521         $circ->fine_interval($recurring->recurance_interval);
1522         $circ->renewal_remaining($duration->max_renewals);
1523
1524     } else {
1525
1526         $logger->info("circulator: copy found with an unlimited circ duration");
1527         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1528         $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1529         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1530         $circ->renewal_remaining(0);
1531     }
1532
1533    $circ->target_copy( $copy->id );
1534    $circ->usr( $patron->id );
1535    $circ->circ_lib( $self->circ_lib );
1536    $circ->workstation($self->editor->requestor->wsid) 
1537     if defined $self->editor->requestor->wsid;
1538
1539    if( $self->is_renewal ) {
1540       $circ->opac_renewal('t') if $self->opac_renewal;
1541       $circ->phone_renewal('t') if $self->phone_renewal;
1542       $circ->desk_renewal('t') if $self->desk_renewal;
1543       $circ->renewal_remaining($self->renewal_remaining);
1544       $circ->circ_staff($self->editor->requestor->id);
1545    }
1546
1547
1548     # if the user provided an overiding checkout time,
1549     # (e.g. the checkout really happened several hours ago), then
1550     # we apply that here.  Does this need a perm??
1551     $circ->xact_start(clense_ISO8601($self->checkout_time))
1552         if $self->checkout_time;
1553
1554     # if a patron is renewing, 'requestor' will be the patron
1555     $circ->circ_staff($self->editor->requestor->id);
1556     $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1557
1558     $self->circ($circ);
1559 }
1560
1561
1562 sub apply_modified_due_date {
1563     my $self = shift;
1564     my $circ = $self->circ;
1565     my $copy = $self->copy;
1566
1567    if( $self->due_date ) {
1568
1569         return $self->bail_on_events($self->editor->event)
1570             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1571
1572       $circ->due_date(clense_ISO8601($self->due_date));
1573
1574    } else {
1575
1576       # if the due_date lands on a day when the location is closed
1577       return unless $copy and $circ->due_date;
1578
1579         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1580
1581         # due-date overlap should be determined by the location the item
1582         # is checked out from, not the owning or circ lib of the item
1583         my $org = $self->editor->requestor->ws_ou;
1584
1585       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1586             " with an item due date of ".$circ->due_date );
1587
1588       my $dateinfo = $U->storagereq(
1589          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1590             $org, $circ->due_date );
1591
1592       if($dateinfo) {
1593          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1594             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1595
1596             # XXX make the behavior more dynamic
1597             # for now, we just push the due date to after the close date
1598             $circ->due_date($dateinfo->{end});
1599       }
1600    }
1601 }
1602
1603
1604
1605 sub create_due_date {
1606     my( $self, $duration ) = @_;
1607
1608     # if there is a raw time component (e.g. from postgres), 
1609     # turn it into an interval that interval_to_seconds can parse
1610     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1611
1612     # for now, use the server timezone.  TODO: use workstation org timezone
1613     my $due_date = DateTime->now(time_zone => 'local');
1614
1615     # add the circ duration
1616     $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
1617
1618     # return ISO8601 time with timezone
1619     return $due_date->strftime('%FT%T%z');
1620 }
1621
1622
1623
1624 sub make_precat_copy {
1625     my $self = shift;
1626     my $copy = $self->copy;
1627
1628    if($copy) {
1629         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1630
1631         $copy->editor($self->editor->requestor->id);
1632         $copy->edit_date('now');
1633         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
1634         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
1635         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
1636         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
1637         $self->update_copy();
1638         return;
1639    }
1640
1641     $logger->info("circulator: Creating a new precataloged ".
1642         "copy in checkout with barcode " . $self->copy_barcode);
1643
1644     $copy = Fieldmapper::asset::copy->new;
1645     $copy->circ_lib($self->circ_lib);
1646     $copy->creator($self->editor->requestor->id);
1647     $copy->editor($self->editor->requestor->id);
1648     $copy->barcode($self->copy_barcode);
1649     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1650     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1651     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1652
1653     $copy->dummy_title($self->dummy_title || "");
1654     $copy->dummy_author($self->dummy_author || "");
1655     $copy->dummy_isbn($self->dummy_isbn || "");
1656     $copy->circ_modifier($self->circ_modifier);
1657
1658
1659     # See if we need to override the circ_lib for the copy with a configured circ_lib
1660     # Setting is shortname of the org unit
1661     my $precat_circ_lib = $U->ou_ancestor_setting_value(
1662         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
1663
1664     if($precat_circ_lib) {
1665         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
1666
1667         if(!$org) {
1668             $self->bail_on_events($self->editor->event);
1669             return;
1670         }
1671
1672         $copy->circ_lib($org->id);
1673     }
1674
1675
1676     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1677         $self->bail_out(1);
1678         $self->push_events($self->editor->event);
1679         return;
1680     }   
1681
1682     # this is a little bit of a hack, but we need to 
1683     # get the copy into the script runner
1684     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1685 }
1686
1687
1688 sub checkout_noncat {
1689     my $self = shift;
1690
1691     my $circ;
1692     my $evt;
1693
1694    my $lib      = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1695    my $count    = $self->noncat_count || 1;
1696    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1697
1698    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
1699
1700    for(1..$count) {
1701
1702       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1703          $self->editor->requestor->id, 
1704             $self->patron->id, 
1705             $lib, 
1706             $self->noncat_type, 
1707             $cotime,
1708             $self->editor );
1709
1710         if( $evt ) {
1711             $self->push_events($evt);
1712             $self->bail_out(1);
1713             return; 
1714         }
1715         $self->circ($circ);
1716    }
1717 }
1718
1719
1720 sub do_checkin {
1721     my $self = shift;
1722     $self->log_me("do_checkin()");
1723
1724     return $self->bail_on_events(
1725         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1726         unless $self->copy;
1727
1728     if( $self->checkin_check_holds_shelf() ) {
1729         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1730         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1731         $self->checkin_flesh_events;
1732         return;
1733     }
1734
1735     unless( $self->is_renewal ) {
1736         return $self->bail_on_events($self->editor->event)
1737             unless $self->editor->allowed('COPY_CHECKIN');
1738     }
1739
1740     $self->push_events($self->check_copy_alert());
1741     $self->push_events($self->check_checkin_copy_status());
1742
1743     # the renew code will have already found our circulation object
1744     unless( $self->is_renewal and $self->circ ) {
1745         my $circs = $self->editor->search_action_circulation(
1746             { target_copy => $self->copy->id, checkin_time => undef });
1747         $self->circ($$circs[0]);
1748
1749         # for now, just warn if there are multiple open circs on a copy
1750         $logger->warn("circulator: we have ".scalar(@$circs).
1751             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1752     }
1753
1754     # run the fine generator against this circ, if this circ is there
1755     $self->generate_fines if ($self->circ);
1756
1757     # if the circ is marked as 'claims returned', add the event to the list
1758     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1759         if ($self->circ and $self->circ->stop_fines 
1760                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1761
1762     $self->check_circ_deposit();
1763
1764     # handle the overridable events 
1765     $self->override_events unless $self->is_renewal;
1766     return if $self->bail_out;
1767     
1768     if( $self->copy ) {
1769         $self->transit(
1770             $self->editor->search_action_transit_copy(
1771             { target_copy => $self->copy->id, dest_recv_time => undef })->[0]); 
1772     }
1773
1774     if( $self->circ ) {
1775         $self->checkin_handle_circ;
1776         return if $self->bail_out;
1777         $self->checkin_changed(1);
1778
1779     } elsif( $self->transit ) {
1780         my $hold_transit = $self->process_received_transit;
1781         $self->checkin_changed(1);
1782
1783         if( $self->bail_out ) { 
1784             $self->checkin_flesh_events;
1785             return;
1786         }
1787         
1788         if( my $e = $self->check_checkin_copy_status() ) {
1789             # If the original copy status is special, alert the caller
1790             my $ev = $self->events;
1791             $self->events([$e]);
1792             $self->override_events;
1793             return if $self->bail_out;
1794             $self->events($ev);
1795         }
1796
1797         if( $hold_transit or 
1798                 $U->copy_status($self->copy->status)->id 
1799                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1800
1801          my $hold;
1802          if( $hold_transit ) {
1803             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1804          } else {
1805                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1806          }
1807
1808             $self->hold($hold);
1809
1810             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1811
1812                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1813                 $self->reshelve_copy(1);
1814                 $self->cancelled_hold_transit(1);
1815                 $self->notify_hold(0); # don't notify for cancelled holds
1816                 return if $self->bail_out;
1817
1818             } else {
1819
1820                 # hold transited to correct location
1821                 $self->checkin_flesh_events;
1822                 return;
1823             }
1824         } 
1825
1826     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1827
1828         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1829             " that is in-transit, but there is no transit.. repairing");
1830         $self->reshelve_copy(1);
1831         return if $self->bail_out;
1832     }
1833
1834     if( $self->is_renewal ) {
1835         $self->push_events(OpenILS::Event->new('SUCCESS'));
1836         return;
1837     }
1838
1839    # ------------------------------------------------------------------------------
1840    # Circulations and transits are now closed where necessary.  Now go on to see if
1841    # this copy can fulfill a hold or needs to be routed to a different location
1842    # ------------------------------------------------------------------------------
1843
1844     unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1845
1846         my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1847         return if $self->bail_out;
1848     
1849         unless($needed_for_hold) {
1850             my $circ_lib = (ref $self->copy->circ_lib) ? 
1851                     $self->copy->circ_lib->id : $self->copy->circ_lib;
1852     
1853             if( $self->remote_hold ) {
1854                 $circ_lib = $self->remote_hold->pickup_lib;
1855                 $logger->warn("circulator: Copy ".$self->copy->barcode.
1856                     " is on a remote hold's shelf, sending to $circ_lib");
1857             }
1858     
1859             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1860     
1861             if( $circ_lib == $self->editor->requestor->ws_ou ) {
1862     
1863                 $self->checkin_handle_precat();
1864                 return if $self->bail_out;
1865     
1866             } else {
1867     
1868                 my $bc = $self->copy->barcode;
1869                 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1870                 $self->checkin_build_copy_transit($circ_lib);
1871                 return if $self->bail_out;
1872                 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1873             }
1874         }
1875     }
1876
1877     $self->reshelve_copy;
1878     return if $self->bail_out;
1879
1880     unless($self->checkin_changed) {
1881
1882         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1883         my $stat = $U->copy_status($self->copy->status)->id;
1884
1885         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1886          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1887         $self->bail_out(1); # no need to commit anything
1888
1889     } else {
1890
1891         $self->push_events(OpenILS::Event->new('SUCCESS')) 
1892             unless @{$self->events};
1893     }
1894
1895     OpenILS::Utils::Penalty->calculate_penalties(
1896         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1897
1898     $self->checkin_flesh_events;
1899     return;
1900 }
1901
1902 # if a deposit was payed for this item, push the event
1903 sub check_circ_deposit {
1904     my $self = shift;
1905     return unless $self->circ;
1906     my $deposit = $self->editor->search_money_billing(
1907         {   btype => 5, 
1908             xact => $self->circ->id, 
1909             voided => 'f'
1910         }, {idlist => 1})->[0];
1911
1912     $self->push_events(OpenILS::Event->new(
1913         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1914 }
1915
1916 sub reshelve_copy {
1917    my $self    = shift;
1918    my $force   = $self->force || shift;
1919    my $copy    = $self->copy;
1920
1921    my $stat = $U->copy_status($copy->status)->id;
1922
1923    if($force || (
1924       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1925       $stat != OILS_COPY_STATUS_CATALOGING and
1926       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1927       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1928
1929         $copy->status( OILS_COPY_STATUS_RESHELVING );
1930             $self->update_copy;
1931             $self->checkin_changed(1);
1932     }
1933 }
1934
1935
1936 # Returns true if the item is at the current location
1937 # because it was transited there for a hold and the 
1938 # hold has not been fulfilled
1939 sub checkin_check_holds_shelf {
1940     my $self = shift;
1941     return 0 unless $self->copy;
1942
1943     return 0 unless 
1944         $U->copy_status($self->copy->status)->id ==
1945             OILS_COPY_STATUS_ON_HOLDS_SHELF;
1946
1947     # find the hold that put us on the holds shelf
1948     my $holds = $self->editor->search_action_hold_request(
1949         { 
1950             current_copy => $self->copy->id,
1951             capture_time => { '!=' => undef },
1952             fulfillment_time => undef,
1953             cancel_time => undef,
1954         }
1955     );
1956
1957     unless(@$holds) {
1958         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1959         $self->reshelve_copy(1);
1960         return 0;
1961     }
1962
1963     my $hold = $$holds[0];
1964
1965     $logger->info("circulator: we found a captured, un-fulfilled hold [".
1966         $hold->id. "] for copy ".$self->copy->barcode);
1967
1968     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1969         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1970         return 1;
1971     }
1972
1973     $logger->info("circulator: hold is not for here..");
1974     $self->remote_hold($hold);
1975     return 0;
1976 }
1977
1978
1979 sub checkin_handle_precat {
1980     my $self    = shift;
1981    my $copy    = $self->copy;
1982
1983    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1984         $copy->status(OILS_COPY_STATUS_CATALOGING);
1985         $self->update_copy();
1986         $self->checkin_changed(1);
1987         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1988    }
1989 }
1990
1991
1992 sub checkin_build_copy_transit {
1993     my $self            = shift;
1994     my $dest            = shift;
1995     my $copy       = $self->copy;
1996    my $transit    = Fieldmapper::action::transit_copy->new;
1997
1998     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1999     $logger->info("circulator: transiting copy to $dest");
2000
2001    $transit->source($self->editor->requestor->ws_ou);
2002    $transit->dest($dest);
2003    $transit->target_copy($copy->id);
2004    $transit->source_send_time('now');
2005    $transit->copy_status( $U->copy_status($copy->status)->id );
2006
2007     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2008
2009     return $self->bail_on_events($self->editor->event)
2010         unless $self->editor->create_action_transit_copy($transit);
2011
2012    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2013     $self->update_copy;
2014     $self->checkin_changed(1);
2015 }
2016
2017
2018 # returns true if the item was used (or may potentially be used 
2019 # in subsequent calls) to capture a hold.
2020 sub attempt_checkin_hold_capture {
2021     my $self = shift;
2022     my $copy = $self->copy;
2023
2024     # we've been explicitly told not to capture any holds
2025     return 0 if $self->capture eq 'nocapture';
2026
2027     # See if this copy can fulfill any holds
2028     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2029         $self->editor, $copy, $self->editor->requestor );
2030
2031     if(!$hold) {
2032         $logger->debug("circulator: no potential permitted".
2033             "holds found for copy ".$copy->barcode);
2034         return 0;
2035     }
2036
2037     if($self->capture ne 'capture') {
2038         # see if this item is in a hold-capture-delay location
2039         my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2040         if($U->is_true($location->hold_verify)) {
2041             $self->bail_on_events(
2042                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2043             return 1;
2044         }
2045     }
2046
2047     $self->retarget($retarget);
2048
2049     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2050
2051     $hold->current_copy($copy->id);
2052     $hold->capture_time('now');
2053     $hold->shelf_time('now') 
2054         if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2055
2056     # prevent DB errors caused by fetching 
2057     # holds from storage, and updating through cstore
2058     $hold->clear_fulfillment_time;
2059     $hold->clear_fulfillment_staff;
2060     $hold->clear_fulfillment_lib;
2061     $hold->clear_expire_time; 
2062     $hold->clear_cancel_time;
2063     $hold->clear_prev_check_time unless $hold->prev_check_time;
2064
2065     $self->bail_on_events($self->editor->event)
2066         unless $self->editor->update_action_hold_request($hold);
2067     $self->hold($hold);
2068     $self->checkin_changed(1);
2069
2070     return 0 if $self->bail_out;
2071
2072     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2073
2074         # This hold was captured in the correct location
2075         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2076         $self->push_events(OpenILS::Event->new('SUCCESS'));
2077
2078         #$self->do_hold_notify($hold->id);
2079         $self->notify_hold($hold->id);
2080
2081     } else {
2082     
2083         # Hold needs to be picked up elsewhere.  Build a hold
2084         # transit and route the item.
2085         $self->checkin_build_hold_transit();
2086         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2087         return 0 if $self->bail_out;
2088         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2089     }
2090
2091     # make sure we save the copy status
2092     $self->update_copy;
2093     return 1;
2094 }
2095
2096 sub do_hold_notify {
2097     my( $self, $holdid ) = @_;
2098
2099     my $e = new_editor(xact => 1);
2100     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2101     $e->rollback;
2102     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2103     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2104
2105     $logger->info("circulator: running delayed hold notify process");
2106
2107 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2108 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2109
2110     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2111         hold_id => $holdid, requestor => $self->editor->requestor);
2112
2113     $logger->debug("circulator: built hold notifier");
2114
2115     if(!$notifier->event) {
2116
2117         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2118
2119         my $stat = $notifier->send_email_notify;
2120         if( $stat == '1' ) {
2121             $logger->info("circulator: hold notify succeeded for hold $holdid");
2122             return;
2123         } 
2124
2125         $logger->warn("circulator:  * hold notify failed for hold $holdid");
2126
2127     } else {
2128         $logger->info("circulator: Not sending hold notification since the patron has no email address");
2129     }
2130 }
2131
2132 sub retarget_holds {
2133     my $self = shift;
2134     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2135     my $ses = OpenSRF::AppSession->create('open-ils.storage');
2136     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2137     # no reason to wait for the return value
2138     return;
2139 }
2140
2141 sub checkin_build_hold_transit {
2142     my $self = shift;
2143
2144    my $copy = $self->copy;
2145    my $hold = $self->hold;
2146    my $trans = Fieldmapper::action::hold_transit_copy->new;
2147
2148     $logger->debug("circulator: building hold transit for ".$copy->barcode);
2149
2150    $trans->hold($hold->id);
2151    $trans->source($self->editor->requestor->ws_ou);
2152    $trans->dest($hold->pickup_lib);
2153    $trans->source_send_time("now");
2154    $trans->target_copy($copy->id);
2155
2156     # when the copy gets to its destination, it will recover
2157     # this status - put it onto the holds shelf
2158    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2159
2160     return $self->bail_on_events($self->editor->event)
2161         unless $self->editor->create_action_hold_transit_copy($trans);
2162 }
2163
2164
2165
2166 sub process_received_transit {
2167     my $self = shift;
2168     my $copy = $self->copy;
2169     my $copyid = $self->copy->id;
2170
2171     my $status_name = $U->copy_status($copy->status)->name;
2172     $logger->debug("circulator: attempting transit receive on ".
2173         "copy $copyid. Copy status is $status_name");
2174
2175     my $transit = $self->transit;
2176
2177     if( $transit->dest != $self->editor->requestor->ws_ou ) {
2178         # - this item is in-transit to a different location
2179
2180         my $tid = $transit->id; 
2181         my $loc = $self->editor->requestor->ws_ou;
2182         my $dest = $transit->dest;
2183
2184         $logger->info("circulator: Fowarding transit on copy which is destined ".
2185             "for a different location. transit=$tid, copy=$copyid, current ".
2186             "location=$loc, destination location=$dest");
2187
2188         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2189
2190         # grab the associated hold object if available
2191         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2192         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2193
2194         return $self->bail_on_events($evt);
2195     }
2196
2197     # The transit is received, set the receive time
2198     $transit->dest_recv_time('now');
2199     $self->bail_on_events($self->editor->event)
2200         unless $self->editor->update_action_transit_copy($transit);
2201
2202     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2203
2204     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2205     $copy->status( $transit->copy_status );
2206     $self->update_copy();
2207     return if $self->bail_out;
2208
2209     my $ishold = 0;
2210     if($hold_transit) { 
2211         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2212
2213         # hold has arrived at destination, set shelf time
2214         $hold->shelf_time('now');
2215         $self->bail_on_events($self->editor->event)
2216             unless $self->editor->update_action_hold_request($hold);
2217         return if $self->bail_out;
2218
2219         $self->notify_hold($hold_transit->hold);
2220         $ishold = 1;
2221     }
2222
2223     $self->push_events( 
2224         OpenILS::Event->new(
2225         'SUCCESS', 
2226         ishold => $ishold,
2227       payload => { transit => $transit, holdtransit => $hold_transit } ));
2228
2229     return $hold_transit;
2230 }
2231
2232
2233 sub generate_fines {
2234    my $self = shift;
2235    my $evt;
2236    my $obt;
2237
2238    my $st = OpenSRF::AppSession->connect('open-ils.storage');
2239
2240    $st->request(
2241       'open-ils.storage.action.circulation.overdue.generate_fines',
2242       undef,
2243       $self->circ->id
2244    )->wait_complete;
2245
2246    $st->disconnect;
2247
2248    # refresh the circ in case the fine generator set the stop_fines field
2249    $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
2250
2251    return undef;
2252 }
2253
2254 sub checkin_handle_circ {
2255    my $self = shift;
2256    my $circ = $self->circ;
2257    my $copy = $self->copy;
2258    my $evt;
2259    my $obt;
2260
2261    # backdate the circ if necessary
2262    if($self->backdate) {
2263         $self->checkin_handle_backdate;
2264         return if $self->bail_out;
2265    }
2266
2267    if(!$circ->stop_fines) {
2268       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2269       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2270       $circ->stop_fines_time('now') unless $self->backdate;
2271       $circ->stop_fines_time($self->backdate) if $self->backdate;
2272    }
2273
2274    # see if there are any fines owed on this circ.  if not, close it
2275     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2276     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2277
2278     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2279
2280     # Set the checkin vars since we have the item
2281     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2282
2283     # capture the true scan time for back-dated checkins
2284     $circ->checkin_scan_time('now');
2285
2286     $circ->checkin_staff($self->editor->requestor->id);
2287     $circ->checkin_lib($self->editor->requestor->ws_ou);
2288     $circ->checkin_workstation($self->editor->requestor->wsid);
2289
2290     my $circ_lib = (ref $self->copy->circ_lib) ?  
2291         $self->copy->circ_lib->id : $self->copy->circ_lib;
2292     my $stat = $U->copy_status($self->copy->status)->id;
2293
2294     # immediately available keeps items lost or missing items from going home before being handled
2295     my $lost_immediately_available = $U->ou_ancestor_setting_value(
2296         $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2297
2298
2299     if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2300
2301         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2302             $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2303         } else {
2304             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2305             $self->update_copy;
2306         }
2307
2308     } elsif ($stat == OILS_COPY_STATUS_LOST) {
2309
2310         $self->checkin_handle_lost($circ_lib);
2311
2312     } else {
2313
2314         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2315         $self->update_copy;
2316     }
2317
2318     return $self->bail_on_events($self->editor->event)
2319         unless $self->editor->update_action_circulation($circ);
2320
2321     # make sure the circ isn't closed if we just voided some fines
2322     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2323     return $self->bail_on_events($evt) if $evt;
2324
2325     return undef;
2326 }
2327
2328
2329 # ------------------------------------------------------------------
2330 # See if we need to void billings for lost checkin
2331 # ------------------------------------------------------------------
2332 sub checkin_handle_lost {
2333     my $self = shift;
2334     my $circ_lib = shift;
2335     my $circ = $self->circ;
2336
2337     my $max_return = $U->ou_ancestor_setting_value(
2338         $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2339
2340     if ($max_return) {
2341
2342         my $today = time();
2343         my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2344         $tm[5] -= 1 if $tm[5] > 0;
2345         my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2346
2347         my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2348         $logger->info("MAX OD: ".$max_return."  DUEDATE: ".$circ->due_date."  TODAY: ".$today."  DUE: ".$due."  LAST: ".$last_chance);
2349
2350         $max_return = 0 if $today < $last_chance;
2351     }
2352
2353     if (!$max_return){  # there's either no max time to accept returns defined or we're within that time
2354
2355         my $void_lost = $U->ou_ancestor_setting_value(
2356             $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2357         my $void_lost_fee = $U->ou_ancestor_setting_value(
2358             $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2359         my $restore_od = $U->ou_ancestor_setting_value(
2360             $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2361
2362         $self->checkin_handle_lost_now_found(3) if $void_lost;
2363         $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2364         $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2365     }
2366
2367     $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2368     $self->update_copy;
2369 }
2370
2371
2372 sub checkin_handle_backdate {
2373     my $self = shift;
2374
2375     my $bd = $self->backdate;
2376
2377     # ------------------------------------------------------------------
2378     # clean up the backdate for date comparison
2379     # we want any bills created on or after the backdate
2380     # ------------------------------------------------------------------
2381     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2382     #$bd = "${bd}T23:59:59";
2383
2384     my $bills = $self->editor->search_money_billing(
2385         { 
2386             billing_ts => { '>=' => $bd }, 
2387             xact => $self->circ->id, 
2388             btype => 1
2389         }
2390     );
2391
2392     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2393
2394     for my $bill (@$bills) {    
2395         unless( $U->is_true($bill->voided) ) {
2396             $logger->info("backdate voiding bill ".$bill->id);
2397             $bill->voided('t');
2398             $bill->void_time('now');
2399             $bill->voider($self->editor->requestor->id);
2400             my $n = $bill->note || "";
2401             $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2402
2403             $self->bail_on_events($self->editor->event)
2404                 unless $self->editor->update_money_billing($bill);
2405         }
2406     }
2407 }
2408
2409
2410
2411
2412 sub find_patron_from_copy {
2413     my $self = shift;
2414     my $circs = $self->editor->search_action_circulation(
2415         { target_copy => $self->copy->id, checkin_time => undef });
2416     my $circ = $circs->[0];
2417     return unless $circ;
2418     my $u = $self->editor->retrieve_actor_user($circ->usr)
2419         or return $self->bail_on_events($self->editor->event);
2420     $self->patron($u);
2421 }
2422
2423 sub check_checkin_copy_status {
2424     my $self = shift;
2425    my $copy = $self->copy;
2426
2427    my $islost     = 0;
2428    my $ismissing  = 0;
2429    my $evt        = undef;
2430
2431    my $status = $U->copy_status($copy->status)->id;
2432
2433    return undef
2434       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
2435             $status == OILS_COPY_STATUS_CHECKED_OUT ||
2436             $status == OILS_COPY_STATUS_IN_PROCESS  ||
2437             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
2438             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
2439             $status == OILS_COPY_STATUS_CATALOGING  ||
2440             $status == OILS_COPY_STATUS_RESHELVING );
2441
2442    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2443       if( $status == OILS_COPY_STATUS_LOST );
2444
2445    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2446       if( $status == OILS_COPY_STATUS_MISSING );
2447
2448    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2449 }
2450
2451
2452
2453 # --------------------------------------------------------------------------
2454 # On checkin, we need to return as many relevant objects as we can
2455 # --------------------------------------------------------------------------
2456 sub checkin_flesh_events {
2457     my $self = shift;
2458
2459     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
2460         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2461             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2462     }
2463
2464     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2465
2466     my $hold;
2467     if($self->hold and !$self->hold->cancel_time) {
2468         $hold = $self->hold;
2469         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
2470     }
2471
2472     for my $evt (@{$self->events}) {
2473
2474         my $payload         = {};
2475         $payload->{copy}    = $U->unflesh_copy($self->copy);
2476         $payload->{record}  = $record,
2477         $payload->{circ}    = $self->circ;
2478         $payload->{transit} = $self->transit;
2479         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2480         $payload->{hold}    = $hold;
2481         $evt->{payload}     = $payload;
2482     }
2483 }
2484
2485 sub log_me {
2486     my( $self, $msg ) = @_;
2487     my $bc = ($self->copy) ? $self->copy->barcode :
2488         $self->barcode;
2489     $bc ||= "";
2490     my $usr = ($self->patron) ? $self->patron->id : "";
2491     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2492         ", recipient=$usr, copy=$bc");
2493 }
2494
2495
2496 sub do_renew {
2497     my $self = shift;
2498     $self->log_me("do_renew()");
2499
2500     # Make sure there is an open circ to renew that is not
2501     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2502     my $usrid = $self->patron->id if $self->patron;
2503     my $circ;
2504     if ($usrid) {
2505         # If we have a patron, match them to the circ
2506         $circ = $self->editor->search_action_circulation(
2507             {target_copy => $self->copy->id, usr => $usrid,  stop_fines => undef})->[0];
2508     } else {
2509         $circ = $self->editor->search_action_circulation(
2510             {target_copy => $self->copy->id, stop_fines => undef})->[0];
2511     }
2512
2513     if(!$circ) {
2514         if ($usrid) {
2515             $circ = $self->editor->search_action_circulation(
2516                 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2517         } else {
2518             $circ = $self->editor->search_action_circulation(
2519                 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2520         }
2521     }
2522
2523     return $self->bail_on_events($self->editor->event) unless $circ;
2524
2525     # A user is not allowed to renew another user's items without permission
2526     unless( $circ->usr eq $self->editor->requestor->id ) {
2527         return $self->bail_on_events($self->editor->events)
2528             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2529     }   
2530
2531     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2532         if $circ->renewal_remaining < 1;
2533
2534     # -----------------------------------------------------------------
2535
2536     $self->renewal_remaining( $circ->renewal_remaining - 1 );
2537     $self->circ($circ);
2538
2539     $self->run_renew_permit;
2540
2541     # Check the item in
2542     $self->do_checkin();
2543     return if $self->bail_out;
2544
2545     unless( $self->permit_override ) {
2546         $self->do_permit();
2547         return if $self->bail_out;
2548         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2549         $self->remove_event('ITEM_NOT_CATALOGED');
2550     }   
2551
2552     $self->override_events;
2553     return if $self->bail_out;
2554
2555     $self->events([]);
2556     $self->do_checkout();
2557 }
2558
2559
2560 sub remove_event {
2561     my( $self, $evt ) = @_;
2562     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2563     $logger->debug("circulator: removing event from list: $evt");
2564     my @events = @{$self->events};
2565     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2566 }
2567
2568
2569 sub have_event {
2570     my( $self, $evt ) = @_;
2571     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2572     return grep { $_->{textcode} eq $evt } @{$self->events};
2573 }
2574
2575
2576
2577 sub run_renew_permit {
2578     my $self = shift;
2579
2580     my $events = [];
2581
2582     if(!$self->legacy_script_support) {
2583         my $results = $self->run_indb_circ_test;
2584         unless($self->circ_test_success) {
2585             push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) for @$results;
2586         }
2587
2588     } else {
2589
2590         my $runner = $self->script_runner;
2591
2592         $runner->load($self->circ_permit_renew);
2593         my $result = $runner->run or 
2594             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2595         $events = $result->{events};
2596         $self->mk_script_runner;
2597     }
2598
2599     $logger->activity("circulator: circ_permit_renew for user ".
2600       $self->patron->id." returned events: @$events") if @$events;
2601
2602     $self->push_events(OpenILS::Event->new($_)) for @$events;
2603
2604     $logger->debug("circulator: re-creating script runner to be safe");
2605 }
2606
2607
2608 sub append_reading_list {
2609     my $self = shift;
2610
2611     return undef unless 
2612         $self->is_checkout and 
2613         $self->patron and 
2614         $self->copy and 
2615         !$self->is_noncat;
2616
2617     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2618
2619     # verify history is globally enabled and uses the bucket mechanism
2620     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2621         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2622
2623     unless($htype eq 'bucket') {
2624         $e->rollback;
2625         return undef;
2626     }
2627
2628     # verify the patron wants to retain the hisory
2629         my $setting = $e->search_actor_user_setting(
2630                 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2631     
2632     unless($setting and $setting->value) {
2633         $e->rollback;
2634         return undef;
2635     }
2636
2637     my $bkt = $e->search_container_copy_bucket(
2638         {owner => $self->patron->id, btype => 'circ_history'})->[0];
2639
2640     my $pos = 1;
2641
2642     if($bkt) {
2643         # find the next item position
2644         my $last_item = $e->search_container_copy_bucket_item(
2645             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2646         $pos = $last_item->pos + 1 if $last_item;
2647
2648     } else {
2649         # create the history bucket if necessary
2650         $bkt = Fieldmapper::container::copy_bucket->new;
2651         $bkt->owner($self->patron->id);
2652         $bkt->name('');
2653         $bkt->btype('circ_history');
2654         $bkt->pub('f');
2655         $e->create_container_copy_bucket($bkt) or return $e->die_event;
2656     }
2657
2658     my $item = Fieldmapper::container::copy_bucket_item->new;
2659
2660     $item->bucket($bkt->id);
2661     $item->target_copy($self->copy->id);
2662     $item->pos($pos);
2663
2664     $e->create_container_copy_bucket_item($item) or return $e->die_event;
2665     $e->commit;
2666
2667     return undef;
2668 }
2669
2670
2671 sub make_trigger_events {
2672     my $self = shift;
2673     return unless $self->circ;
2674     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2675     $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2676     $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2677     # ignore response
2678 }
2679
2680
2681
2682 sub checkin_handle_lost_now_found {
2683     my ($self, $bill_type) = @_;
2684
2685     # ------------------------------------------------------------------
2686     # remove charge from patron's account if lost item is returned
2687     # ------------------------------------------------------------------
2688
2689     my $bills = $self->editor->search_money_billing(
2690         {
2691             xact => $self->circ->id,
2692             btype => $bill_type
2693         }
2694     );
2695
2696     $logger->debug("voiding lost item charge of  ".scalar(@$bills));
2697     for my $bill (@$bills) {
2698         if( !$U->is_true($bill->voided) ) {
2699             $logger->info("lost item returned - voiding bill ".$bill->id);
2700             $bill->voided('t');
2701             $bill->void_time('now');
2702             $bill->voider($self->editor->requestor->id);
2703             my $note = ($bill->note) ? $bill->note . "\n" : '';
2704             $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2705
2706             $self->bail_on_events($self->editor->event)
2707                 unless $self->editor->update_money_billing($bill);
2708         }
2709     }
2710 }
2711
2712 sub checkin_handle_lost_now_found_restore_od {
2713     my $self = shift;
2714
2715     # ------------------------------------------------------------------
2716     # restore those overdue charges voided when item was set to lost
2717     # ------------------------------------------------------------------
2718
2719     my $ods = $self->editor->search_money_billing(
2720         {
2721                 xact => $self->circ->id,
2722                 btype => 1
2723         }
2724     );
2725
2726     $logger->debug("returning overdue charges pre-lost  ".scalar(@$ods));
2727     for my $bill (@$ods) {
2728         if( $U->is_true($bill->voided) ) {
2729                 $logger->info("lost item returned - restoring overdue ".$bill->id);
2730                 $bill->voided('f');
2731                 $bill->clear_void_time;
2732                 $bill->voider($self->editor->requestor->id);
2733                 my $note = ($bill->note) ? $bill->note . "\n" : '';
2734                 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2735
2736                 $self->bail_on_events($self->editor->event)
2737                         unless $self->editor->update_money_billing($bill);
2738         }
2739     }
2740 }
2741
2742