]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
oops, variable typo.
[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     # if there is a raw time component (e.g. from postgres), 
1608     # turn it into an interval that interval_to_seconds can parse
1609     $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
1610    my ($sec,$min,$hour,$mday,$mon,$year) =
1611       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1612    $year += 1900; $mon += 1;
1613    my $due_date = sprintf(
1614       '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1615       $year, $mon, $mday, $hour, $min, $sec);
1616    return $due_date;
1617 }
1618
1619
1620
1621 sub make_precat_copy {
1622     my $self = shift;
1623     my $copy = $self->copy;
1624
1625    if($copy) {
1626         $logger->debug("circulator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1627
1628         $copy->editor($self->editor->requestor->id);
1629         $copy->edit_date('now');
1630         $copy->dummy_title($self->dummy_title || $copy->dummy_title || '');
1631         $copy->dummy_isbn($self->dummy_isbn || $copy->dummy_isbn || '');
1632         $copy->dummy_author($self->dummy_author || $copy->dummy_author || '');
1633         $copy->circ_modifier($self->circ_modifier || $copy->circ_modifier);
1634         $self->update_copy();
1635         return;
1636    }
1637
1638     $logger->info("circulator: Creating a new precataloged ".
1639         "copy in checkout with barcode " . $self->copy_barcode);
1640
1641     $copy = Fieldmapper::asset::copy->new;
1642     $copy->circ_lib($self->circ_lib);
1643     $copy->creator($self->editor->requestor->id);
1644     $copy->editor($self->editor->requestor->id);
1645     $copy->barcode($self->copy_barcode);
1646     $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1647     $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1648     $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1649
1650     $copy->dummy_title($self->dummy_title || "");
1651     $copy->dummy_author($self->dummy_author || "");
1652     $copy->dummy_isbn($self->dummy_isbn || "");
1653     $copy->circ_modifier($self->circ_modifier);
1654
1655
1656     # See if we need to override the circ_lib for the copy with a configured circ_lib
1657     # Setting is shortname of the org unit
1658     my $precat_circ_lib = $U->ou_ancestor_setting_value(
1659         $self->circ_lib, 'circ.pre_cat_copy_circ_lib', $self->editor);
1660
1661     if($precat_circ_lib) {
1662         my $org = $self->editor->search_actor_org_unit({shortname => $precat_circ_lib})->[0];
1663
1664         if(!$org) {
1665             $self->bail_on_events($self->editor->event);
1666             return;
1667         }
1668
1669         $copy->circ_lib($org->id);
1670     }
1671
1672
1673     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1674         $self->bail_out(1);
1675         $self->push_events($self->editor->event);
1676         return;
1677     }   
1678
1679     # this is a little bit of a hack, but we need to 
1680     # get the copy into the script runner
1681     $self->script_runner->insert("environment.copy", $copy, 1) if $self->script_runner;
1682 }
1683
1684
1685 sub checkout_noncat {
1686     my $self = shift;
1687
1688     my $circ;
1689     my $evt;
1690
1691    my $lib      = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1692    my $count    = $self->noncat_count || 1;
1693    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1694
1695    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
1696
1697    for(1..$count) {
1698
1699       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1700          $self->editor->requestor->id, 
1701             $self->patron->id, 
1702             $lib, 
1703             $self->noncat_type, 
1704             $cotime,
1705             $self->editor );
1706
1707         if( $evt ) {
1708             $self->push_events($evt);
1709             $self->bail_out(1);
1710             return; 
1711         }
1712         $self->circ($circ);
1713    }
1714 }
1715
1716
1717 sub do_checkin {
1718     my $self = shift;
1719     $self->log_me("do_checkin()");
1720
1721     return $self->bail_on_events(
1722         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1723         unless $self->copy;
1724
1725     if( $self->checkin_check_holds_shelf() ) {
1726         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1727         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1728         $self->checkin_flesh_events;
1729         return;
1730     }
1731
1732     unless( $self->is_renewal ) {
1733         return $self->bail_on_events($self->editor->event)
1734             unless $self->editor->allowed('COPY_CHECKIN');
1735     }
1736
1737     $self->push_events($self->check_copy_alert());
1738     $self->push_events($self->check_checkin_copy_status());
1739
1740     # the renew code will have already found our circulation object
1741     unless( $self->is_renewal and $self->circ ) {
1742         my $circs = $self->editor->search_action_circulation(
1743             { target_copy => $self->copy->id, checkin_time => undef });
1744         $self->circ($$circs[0]);
1745
1746         # for now, just warn if there are multiple open circs on a copy
1747         $logger->warn("circulator: we have ".scalar(@$circs).
1748             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1749     }
1750
1751     # run the fine generator against this circ, if this circ is there
1752     $self->generate_fines if ($self->circ);
1753
1754     # if the circ is marked as 'claims returned', add the event to the list
1755     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1756         if ($self->circ and $self->circ->stop_fines 
1757                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1758
1759     $self->check_circ_deposit();
1760
1761     # handle the overridable events 
1762     $self->override_events unless $self->is_renewal;
1763     return if $self->bail_out;
1764     
1765     if( $self->copy ) {
1766         $self->transit(
1767             $self->editor->search_action_transit_copy(
1768             { target_copy => $self->copy->id, dest_recv_time => undef })->[0]); 
1769     }
1770
1771     if( $self->circ ) {
1772         $self->checkin_handle_circ;
1773         return if $self->bail_out;
1774         $self->checkin_changed(1);
1775
1776     } elsif( $self->transit ) {
1777         my $hold_transit = $self->process_received_transit;
1778         $self->checkin_changed(1);
1779
1780         if( $self->bail_out ) { 
1781             $self->checkin_flesh_events;
1782             return;
1783         }
1784         
1785         if( my $e = $self->check_checkin_copy_status() ) {
1786             # If the original copy status is special, alert the caller
1787             my $ev = $self->events;
1788             $self->events([$e]);
1789             $self->override_events;
1790             return if $self->bail_out;
1791             $self->events($ev);
1792         }
1793
1794         if( $hold_transit or 
1795                 $U->copy_status($self->copy->status)->id 
1796                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1797
1798          my $hold;
1799          if( $hold_transit ) {
1800             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1801          } else {
1802                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1803          }
1804
1805             $self->hold($hold);
1806
1807             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1808
1809                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1810                 $self->reshelve_copy(1);
1811                 $self->cancelled_hold_transit(1);
1812                 $self->notify_hold(0); # don't notify for cancelled holds
1813                 return if $self->bail_out;
1814
1815             } else {
1816
1817                 # hold transited to correct location
1818                 $self->checkin_flesh_events;
1819                 return;
1820             }
1821         } 
1822
1823     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1824
1825         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1826             " that is in-transit, but there is no transit.. repairing");
1827         $self->reshelve_copy(1);
1828         return if $self->bail_out;
1829     }
1830
1831     if( $self->is_renewal ) {
1832         $self->push_events(OpenILS::Event->new('SUCCESS'));
1833         return;
1834     }
1835
1836    # ------------------------------------------------------------------------------
1837    # Circulations and transits are now closed where necessary.  Now go on to see if
1838    # this copy can fulfill a hold or needs to be routed to a different location
1839    # ------------------------------------------------------------------------------
1840
1841     unless($self->noop) { # no-op checkins to not capture holds or put items into transit
1842
1843         my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
1844         return if $self->bail_out;
1845     
1846         unless($needed_for_hold) {
1847             my $circ_lib = (ref $self->copy->circ_lib) ? 
1848                     $self->copy->circ_lib->id : $self->copy->circ_lib;
1849     
1850             if( $self->remote_hold ) {
1851                 $circ_lib = $self->remote_hold->pickup_lib;
1852                 $logger->warn("circulator: Copy ".$self->copy->barcode.
1853                     " is on a remote hold's shelf, sending to $circ_lib");
1854             }
1855     
1856             $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1857     
1858             if( $circ_lib == $self->editor->requestor->ws_ou ) {
1859     
1860                 $self->checkin_handle_precat();
1861                 return if $self->bail_out;
1862     
1863             } else {
1864     
1865                 my $bc = $self->copy->barcode;
1866                 $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1867                 $self->checkin_build_copy_transit($circ_lib);
1868                 return if $self->bail_out;
1869                 $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1870             }
1871         }
1872     }
1873
1874     $self->reshelve_copy;
1875     return if $self->bail_out;
1876
1877     unless($self->checkin_changed) {
1878
1879         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1880         my $stat = $U->copy_status($self->copy->status)->id;
1881
1882         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1883          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1884         $self->bail_out(1); # no need to commit anything
1885
1886     } else {
1887
1888         $self->push_events(OpenILS::Event->new('SUCCESS')) 
1889             unless @{$self->events};
1890     }
1891
1892     OpenILS::Utils::Penalty->calculate_penalties(
1893         $self->editor, $self->patron->id, $self->circ_lib) if $self->patron;
1894
1895     $self->checkin_flesh_events;
1896     return;
1897 }
1898
1899 # if a deposit was payed for this item, push the event
1900 sub check_circ_deposit {
1901     my $self = shift;
1902     return unless $self->circ;
1903     my $deposit = $self->editor->search_money_billing(
1904         {   btype => 5, 
1905             xact => $self->circ->id, 
1906             voided => 'f'
1907         }, {idlist => 1})->[0];
1908
1909     $self->push_events(OpenILS::Event->new(
1910         'ITEM_DEPOSIT_PAID', payload => $deposit)) if $deposit;
1911 }
1912
1913 sub reshelve_copy {
1914    my $self    = shift;
1915    my $force   = $self->force || shift;
1916    my $copy    = $self->copy;
1917
1918    my $stat = $U->copy_status($copy->status)->id;
1919
1920    if($force || (
1921       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1922       $stat != OILS_COPY_STATUS_CATALOGING and
1923       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1924       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1925
1926         $copy->status( OILS_COPY_STATUS_RESHELVING );
1927             $self->update_copy;
1928             $self->checkin_changed(1);
1929     }
1930 }
1931
1932
1933 # Returns true if the item is at the current location
1934 # because it was transited there for a hold and the 
1935 # hold has not been fulfilled
1936 sub checkin_check_holds_shelf {
1937     my $self = shift;
1938     return 0 unless $self->copy;
1939
1940     return 0 unless 
1941         $U->copy_status($self->copy->status)->id ==
1942             OILS_COPY_STATUS_ON_HOLDS_SHELF;
1943
1944     # find the hold that put us on the holds shelf
1945     my $holds = $self->editor->search_action_hold_request(
1946         { 
1947             current_copy => $self->copy->id,
1948             capture_time => { '!=' => undef },
1949             fulfillment_time => undef,
1950             cancel_time => undef,
1951         }
1952     );
1953
1954     unless(@$holds) {
1955         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1956         $self->reshelve_copy(1);
1957         return 0;
1958     }
1959
1960     my $hold = $$holds[0];
1961
1962     $logger->info("circulator: we found a captured, un-fulfilled hold [".
1963         $hold->id. "] for copy ".$self->copy->barcode);
1964
1965     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1966         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1967         return 1;
1968     }
1969
1970     $logger->info("circulator: hold is not for here..");
1971     $self->remote_hold($hold);
1972     return 0;
1973 }
1974
1975
1976 sub checkin_handle_precat {
1977     my $self    = shift;
1978    my $copy    = $self->copy;
1979
1980    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1981         $copy->status(OILS_COPY_STATUS_CATALOGING);
1982         $self->update_copy();
1983         $self->checkin_changed(1);
1984         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1985    }
1986 }
1987
1988
1989 sub checkin_build_copy_transit {
1990     my $self            = shift;
1991     my $dest            = shift;
1992     my $copy       = $self->copy;
1993    my $transit    = Fieldmapper::action::transit_copy->new;
1994
1995     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1996     $logger->info("circulator: transiting copy to $dest");
1997
1998    $transit->source($self->editor->requestor->ws_ou);
1999    $transit->dest($dest);
2000    $transit->target_copy($copy->id);
2001    $transit->source_send_time('now');
2002    $transit->copy_status( $U->copy_status($copy->status)->id );
2003
2004     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
2005
2006     return $self->bail_on_events($self->editor->event)
2007         unless $self->editor->create_action_transit_copy($transit);
2008
2009    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2010     $self->update_copy;
2011     $self->checkin_changed(1);
2012 }
2013
2014
2015 # returns true if the item was used (or may potentially be used 
2016 # in subsequent calls) to capture a hold.
2017 sub attempt_checkin_hold_capture {
2018     my $self = shift;
2019     my $copy = $self->copy;
2020
2021     # we've been explicitly told not to capture any holds
2022     return 0 if $self->capture eq 'nocapture';
2023
2024     # See if this copy can fulfill any holds
2025     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
2026         $self->editor, $copy, $self->editor->requestor );
2027
2028     if(!$hold) {
2029         $logger->debug("circulator: no potential permitted".
2030             "holds found for copy ".$copy->barcode);
2031         return 0;
2032     }
2033
2034     if($self->capture ne 'capture') {
2035         # see if this item is in a hold-capture-delay location
2036         my $location = $self->editor->retrieve_asset_copy_location($self->copy->location);
2037         if($U->is_true($location->hold_verify)) {
2038             $self->bail_on_events(
2039                 OpenILS::Event->new('HOLD_CAPTURE_DELAYED', copy_location => $location));
2040             return 1;
2041         }
2042     }
2043
2044     $self->retarget($retarget);
2045
2046     $logger->info("circulator: found permitted hold ".$hold->id." for copy, capturing...");
2047
2048     $hold->current_copy($copy->id);
2049     $hold->capture_time('now');
2050     $hold->shelf_time('now') 
2051         if $hold->pickup_lib == $self->editor->requestor->ws_ou;
2052
2053     # prevent DB errors caused by fetching 
2054     # holds from storage, and updating through cstore
2055     $hold->clear_fulfillment_time;
2056     $hold->clear_fulfillment_staff;
2057     $hold->clear_fulfillment_lib;
2058     $hold->clear_expire_time; 
2059     $hold->clear_cancel_time;
2060     $hold->clear_prev_check_time unless $hold->prev_check_time;
2061
2062     $self->bail_on_events($self->editor->event)
2063         unless $self->editor->update_action_hold_request($hold);
2064     $self->hold($hold);
2065     $self->checkin_changed(1);
2066
2067     return 0 if $self->bail_out;
2068
2069     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
2070
2071         # This hold was captured in the correct location
2072         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2073         $self->push_events(OpenILS::Event->new('SUCCESS'));
2074
2075         #$self->do_hold_notify($hold->id);
2076         $self->notify_hold($hold->id);
2077
2078     } else {
2079     
2080         # Hold needs to be picked up elsewhere.  Build a hold
2081         # transit and route the item.
2082         $self->checkin_build_hold_transit();
2083         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
2084         return 0 if $self->bail_out;
2085         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
2086     }
2087
2088     # make sure we save the copy status
2089     $self->update_copy;
2090     return 1;
2091 }
2092
2093 sub do_hold_notify {
2094     my( $self, $holdid ) = @_;
2095
2096     my $e = new_editor(xact => 1);
2097     my $hold = $e->retrieve_action_hold_request($holdid) or return $e->die_event;
2098     $e->rollback;
2099     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2100     $ses->request('open-ils.trigger.event.autocreate', 'hold.available', $hold, $hold->pickup_lib);
2101
2102     $logger->info("circulator: running delayed hold notify process");
2103
2104 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2105 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
2106
2107     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
2108         hold_id => $holdid, requestor => $self->editor->requestor);
2109
2110     $logger->debug("circulator: built hold notifier");
2111
2112     if(!$notifier->event) {
2113
2114         $logger->info("circulator: attempt at sending hold notification for hold $holdid");
2115
2116         my $stat = $notifier->send_email_notify;
2117         if( $stat == '1' ) {
2118             $logger->info("circulator: hold notify succeeded for hold $holdid");
2119             return;
2120         } 
2121
2122         $logger->warn("circulator:  * hold notify failed for hold $holdid");
2123
2124     } else {
2125         $logger->info("circulator: Not sending hold notification since the patron has no email address");
2126     }
2127 }
2128
2129 sub retarget_holds {
2130     my $self = shift;
2131     $logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
2132     my $ses = OpenSRF::AppSession->create('open-ils.storage');
2133     $ses->request('open-ils.storage.action.hold_request.copy_targeter', undef, $self->retarget);
2134     # no reason to wait for the return value
2135     return;
2136 }
2137
2138 sub checkin_build_hold_transit {
2139     my $self = shift;
2140
2141    my $copy = $self->copy;
2142    my $hold = $self->hold;
2143    my $trans = Fieldmapper::action::hold_transit_copy->new;
2144
2145     $logger->debug("circulator: building hold transit for ".$copy->barcode);
2146
2147    $trans->hold($hold->id);
2148    $trans->source($self->editor->requestor->ws_ou);
2149    $trans->dest($hold->pickup_lib);
2150    $trans->source_send_time("now");
2151    $trans->target_copy($copy->id);
2152
2153     # when the copy gets to its destination, it will recover
2154     # this status - put it onto the holds shelf
2155    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
2156
2157     return $self->bail_on_events($self->editor->event)
2158         unless $self->editor->create_action_hold_transit_copy($trans);
2159 }
2160
2161
2162
2163 sub process_received_transit {
2164     my $self = shift;
2165     my $copy = $self->copy;
2166     my $copyid = $self->copy->id;
2167
2168     my $status_name = $U->copy_status($copy->status)->name;
2169     $logger->debug("circulator: attempting transit receive on ".
2170         "copy $copyid. Copy status is $status_name");
2171
2172     my $transit = $self->transit;
2173
2174     if( $transit->dest != $self->editor->requestor->ws_ou ) {
2175         # - this item is in-transit to a different location
2176
2177         my $tid = $transit->id; 
2178         my $loc = $self->editor->requestor->ws_ou;
2179         my $dest = $transit->dest;
2180
2181         $logger->info("circulator: Fowarding transit on copy which is destined ".
2182             "for a different location. transit=$tid, copy=$copyid, current ".
2183             "location=$loc, destination location=$dest");
2184
2185         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
2186
2187         # grab the associated hold object if available
2188         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
2189         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
2190
2191         return $self->bail_on_events($evt);
2192     }
2193
2194     # The transit is received, set the receive time
2195     $transit->dest_recv_time('now');
2196     $self->bail_on_events($self->editor->event)
2197         unless $self->editor->update_action_transit_copy($transit);
2198
2199     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
2200
2201     $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
2202     $copy->status( $transit->copy_status );
2203     $self->update_copy();
2204     return if $self->bail_out;
2205
2206     my $ishold = 0;
2207     if($hold_transit) { 
2208         my $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
2209
2210         # hold has arrived at destination, set shelf time
2211         $hold->shelf_time('now');
2212         $self->bail_on_events($self->editor->event)
2213             unless $self->editor->update_action_hold_request($hold);
2214         return if $self->bail_out;
2215
2216         $self->notify_hold($hold_transit->hold);
2217         $ishold = 1;
2218     }
2219
2220     $self->push_events( 
2221         OpenILS::Event->new(
2222         'SUCCESS', 
2223         ishold => $ishold,
2224       payload => { transit => $transit, holdtransit => $hold_transit } ));
2225
2226     return $hold_transit;
2227 }
2228
2229
2230 sub generate_fines {
2231    my $self = shift;
2232    my $evt;
2233    my $obt;
2234
2235    my $st = OpenSRF::AppSession->connect('open-ils.storage');
2236
2237    $st->request(
2238       'open-ils.storage.action.circulation.overdue.generate_fines',
2239       undef,
2240       $self->circ->id
2241    )->wait_complete;
2242
2243    $st->disconnect;
2244
2245    # refresh the circ in case the fine generator set the stop_fines field
2246    $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
2247
2248    return undef;
2249 }
2250
2251 sub checkin_handle_circ {
2252    my $self = shift;
2253    my $circ = $self->circ;
2254    my $copy = $self->copy;
2255    my $evt;
2256    my $obt;
2257
2258    # backdate the circ if necessary
2259    if($self->backdate) {
2260         $self->checkin_handle_backdate;
2261         return if $self->bail_out;
2262    }
2263
2264    if(!$circ->stop_fines) {
2265       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
2266       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
2267       $circ->stop_fines_time('now') unless $self->backdate;
2268       $circ->stop_fines_time($self->backdate) if $self->backdate;
2269    }
2270
2271    # see if there are any fines owed on this circ.  if not, close it
2272     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
2273     $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
2274
2275     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
2276
2277     # Set the checkin vars since we have the item
2278     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
2279
2280     # capture the true scan time for back-dated checkins
2281     $circ->checkin_scan_time('now');
2282
2283     $circ->checkin_staff($self->editor->requestor->id);
2284     $circ->checkin_lib($self->editor->requestor->ws_ou);
2285     $circ->checkin_workstation($self->editor->requestor->wsid);
2286
2287     my $circ_lib = (ref $self->copy->circ_lib) ?  
2288         $self->copy->circ_lib->id : $self->copy->circ_lib;
2289     my $stat = $U->copy_status($self->copy->status)->id;
2290
2291     # immediately available keeps items lost or missing items from going home before being handled
2292     my $lost_immediately_available = $U->ou_ancestor_setting_value(
2293         $circ_lib, OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE, $self->editor) || 0;
2294
2295
2296     if ( (!$lost_immediately_available) && ($circ_lib != $self->editor->requestor->ws_ou) ) {
2297
2298         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING) ) {
2299             $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
2300         } else {
2301             $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2302             $self->update_copy;
2303         }
2304
2305     } elsif ($stat == OILS_COPY_STATUS_LOST) {
2306
2307         $self->checkin_handle_lost($circ_lib);
2308
2309     } else {
2310
2311         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2312         $self->update_copy;
2313     }
2314
2315     return $self->bail_on_events($self->editor->event)
2316         unless $self->editor->update_action_circulation($circ);
2317
2318     # make sure the circ isn't closed if we just voided some fines
2319     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($self->editor, $circ->id);
2320     return $self->bail_on_events($evt) if $evt;
2321
2322     return undef;
2323 }
2324
2325
2326 # ------------------------------------------------------------------
2327 # See if we need to void billings for lost checkin
2328 # ------------------------------------------------------------------
2329 sub checkin_handle_lost {
2330     my $self = shift;
2331     my $circ_lib = shift;
2332     my $circ = $self->circ;
2333
2334     my $max_return = $U->ou_ancestor_setting_value(
2335         $circ_lib, OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST, $self->editor) || 0;
2336
2337     if ($max_return) {
2338
2339         my $today = time();
2340         my @tm = reverse($circ->due_date =~ /([\d\.]+)/og);
2341         $tm[5] -= 1 if $tm[5] > 0;
2342         my $due = timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
2343
2344         my $last_chance = OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
2345         $logger->info("MAX OD: ".$max_return."  DUEDATE: ".$circ->due_date."  TODAY: ".$today."  DUE: ".$due."  LAST: ".$last_chance);
2346
2347         $max_return = 0 if $today < $last_chance;
2348     }
2349
2350     if (!$max_return){  # there's either no max time to accept returns defined or we're within that time
2351
2352         my $void_lost = $U->ou_ancestor_setting_value(
2353             $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
2354         my $void_lost_fee = $U->ou_ancestor_setting_value(
2355             $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
2356         my $restore_od = $U->ou_ancestor_setting_value(
2357             $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
2358
2359         $self->checkin_handle_lost_now_found(3) if $void_lost;
2360         $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
2361         $self->checkin_handle_lost_now_found_restore_od() if $restore_od;
2362     }
2363
2364     $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
2365     $self->update_copy;
2366 }
2367
2368
2369 sub checkin_handle_backdate {
2370     my $self = shift;
2371
2372     my $bd = $self->backdate;
2373
2374     # ------------------------------------------------------------------
2375     # clean up the backdate for date comparison
2376     # we want any bills created on or after the backdate
2377     # ------------------------------------------------------------------
2378     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
2379     #$bd = "${bd}T23:59:59";
2380
2381     my $bills = $self->editor->search_money_billing(
2382         { 
2383             billing_ts => { '>=' => $bd }, 
2384             xact => $self->circ->id, 
2385             btype => 1
2386         }
2387     );
2388
2389     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
2390
2391     for my $bill (@$bills) {    
2392         unless( $U->is_true($bill->voided) ) {
2393             $logger->info("backdate voiding bill ".$bill->id);
2394             $bill->voided('t');
2395             $bill->void_time('now');
2396             $bill->voider($self->editor->requestor->id);
2397             my $n = $bill->note || "";
2398             $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
2399
2400             $self->bail_on_events($self->editor->event)
2401                 unless $self->editor->update_money_billing($bill);
2402         }
2403     }
2404 }
2405
2406
2407
2408
2409 sub find_patron_from_copy {
2410     my $self = shift;
2411     my $circs = $self->editor->search_action_circulation(
2412         { target_copy => $self->copy->id, checkin_time => undef });
2413     my $circ = $circs->[0];
2414     return unless $circ;
2415     my $u = $self->editor->retrieve_actor_user($circ->usr)
2416         or return $self->bail_on_events($self->editor->event);
2417     $self->patron($u);
2418 }
2419
2420 sub check_checkin_copy_status {
2421     my $self = shift;
2422    my $copy = $self->copy;
2423
2424    my $islost     = 0;
2425    my $ismissing  = 0;
2426    my $evt        = undef;
2427
2428    my $status = $U->copy_status($copy->status)->id;
2429
2430    return undef
2431       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
2432             $status == OILS_COPY_STATUS_CHECKED_OUT ||
2433             $status == OILS_COPY_STATUS_IN_PROCESS  ||
2434             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
2435             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
2436             $status == OILS_COPY_STATUS_CATALOGING  ||
2437             $status == OILS_COPY_STATUS_RESHELVING );
2438
2439    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
2440       if( $status == OILS_COPY_STATUS_LOST );
2441
2442    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
2443       if( $status == OILS_COPY_STATUS_MISSING );
2444
2445    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
2446 }
2447
2448
2449
2450 # --------------------------------------------------------------------------
2451 # On checkin, we need to return as many relevant objects as we can
2452 # --------------------------------------------------------------------------
2453 sub checkin_flesh_events {
2454     my $self = shift;
2455
2456     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
2457         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
2458             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
2459     }
2460
2461     my $record = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
2462
2463     my $hold;
2464     if($self->hold and !$self->hold->cancel_time) {
2465         $hold = $self->hold;
2466         $hold->notes($self->editor->search_action_hold_request_note({hold => $hold->id}));
2467     }
2468
2469     for my $evt (@{$self->events}) {
2470
2471         my $payload         = {};
2472         $payload->{copy}    = $U->unflesh_copy($self->copy);
2473         $payload->{record}  = $record,
2474         $payload->{circ}    = $self->circ;
2475         $payload->{transit} = $self->transit;
2476         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
2477         $payload->{hold}    = $hold;
2478         $evt->{payload}     = $payload;
2479     }
2480 }
2481
2482 sub log_me {
2483     my( $self, $msg ) = @_;
2484     my $bc = ($self->copy) ? $self->copy->barcode :
2485         $self->barcode;
2486     $bc ||= "";
2487     my $usr = ($self->patron) ? $self->patron->id : "";
2488     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
2489         ", recipient=$usr, copy=$bc");
2490 }
2491
2492
2493 sub do_renew {
2494     my $self = shift;
2495     $self->log_me("do_renew()");
2496
2497     # Make sure there is an open circ to renew that is not
2498     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
2499     my $usrid = $self->patron->id if $self->patron;
2500     my $circ;
2501     if ($usrid) {
2502         # If we have a patron, match them to the circ
2503         $circ = $self->editor->search_action_circulation(
2504             {target_copy => $self->copy->id, usr => $usrid,  stop_fines => undef})->[0];
2505     } else {
2506         $circ = $self->editor->search_action_circulation(
2507             {target_copy => $self->copy->id, stop_fines => undef})->[0];
2508     }
2509
2510     if(!$circ) {
2511         if ($usrid) {
2512             $circ = $self->editor->search_action_circulation(
2513                 {target_copy => $self->copy->id, usr => $usrid, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2514         } else {
2515             $circ = $self->editor->search_action_circulation(
2516                 {target_copy => $self->copy->id, stop_fines => OILS_STOP_FINES_MAX_FINES, checkin_time => undef})->[0];
2517         }
2518     }
2519
2520     return $self->bail_on_events($self->editor->event) unless $circ;
2521
2522     # A user is not allowed to renew another user's items without permission
2523     unless( $circ->usr eq $self->editor->requestor->id ) {
2524         return $self->bail_on_events($self->editor->events)
2525             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
2526     }   
2527
2528     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
2529         if $circ->renewal_remaining < 1;
2530
2531     # -----------------------------------------------------------------
2532
2533     $self->renewal_remaining( $circ->renewal_remaining - 1 );
2534     $self->circ($circ);
2535
2536     $self->run_renew_permit;
2537
2538     # Check the item in
2539     $self->do_checkin();
2540     return if $self->bail_out;
2541
2542     unless( $self->permit_override ) {
2543         $self->do_permit();
2544         return if $self->bail_out;
2545         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
2546         $self->remove_event('ITEM_NOT_CATALOGED');
2547     }   
2548
2549     $self->override_events;
2550     return if $self->bail_out;
2551
2552     $self->events([]);
2553     $self->do_checkout();
2554 }
2555
2556
2557 sub remove_event {
2558     my( $self, $evt ) = @_;
2559     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2560     $logger->debug("circulator: removing event from list: $evt");
2561     my @events = @{$self->events};
2562     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2563 }
2564
2565
2566 sub have_event {
2567     my( $self, $evt ) = @_;
2568     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2569     return grep { $_->{textcode} eq $evt } @{$self->events};
2570 }
2571
2572
2573
2574 sub run_renew_permit {
2575     my $self = shift;
2576
2577     my $events = [];
2578
2579     if(!$self->legacy_script_support) {
2580         my $results = $self->run_indb_circ_test;
2581         unless($self->circ_test_success) {
2582             push(@$events, $LEGACY_CIRC_EVENT_MAP->{$_->{fail_part}} || $_->{fail_part}) for @$results;
2583         }
2584
2585     } else {
2586
2587         my $runner = $self->script_runner;
2588
2589         $runner->load($self->circ_permit_renew);
2590         my $result = $runner->run or 
2591             throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2592         $events = $result->{events};
2593         $self->mk_script_runner;
2594     }
2595
2596     $logger->activity("circulator: circ_permit_renew for user ".
2597       $self->patron->id." returned events: @$events") if @$events;
2598
2599     $self->push_events(OpenILS::Event->new($_)) for @$events;
2600
2601     $logger->debug("circulator: re-creating script runner to be safe");
2602 }
2603
2604
2605 sub append_reading_list {
2606     my $self = shift;
2607
2608     return undef unless 
2609         $self->is_checkout and 
2610         $self->patron and 
2611         $self->copy and 
2612         !$self->is_noncat;
2613
2614     my $e = new_editor(xact => 1, requestor => $self->editor->requestor);
2615
2616     # verify history is globally enabled and uses the bucket mechanism
2617     my $htype = OpenSRF::Utils::SettingsClient->new->config_value(
2618         apps => 'open-ils.circ' => app_settings => 'checkout_history_mechanism');
2619
2620     unless($htype eq 'bucket') {
2621         $e->rollback;
2622         return undef;
2623     }
2624
2625     # verify the patron wants to retain the hisory
2626         my $setting = $e->search_actor_user_setting(
2627                 {usr => $self->patron->id, name => 'circ.keep_checkout_history'})->[0];
2628     
2629     unless($setting and $setting->value) {
2630         $e->rollback;
2631         return undef;
2632     }
2633
2634     my $bkt = $e->search_container_copy_bucket(
2635         {owner => $self->patron->id, btype => 'circ_history'})->[0];
2636
2637     my $pos = 1;
2638
2639     if($bkt) {
2640         # find the next item position
2641         my $last_item = $e->search_container_copy_bucket_item(
2642             {bucket => $bkt->id}, {order_by => {ccbi => 'pos desc'}, limit => 1})->[0];
2643         $pos = $last_item->pos + 1 if $last_item;
2644
2645     } else {
2646         # create the history bucket if necessary
2647         $bkt = Fieldmapper::container::copy_bucket->new;
2648         $bkt->owner($self->patron->id);
2649         $bkt->name('');
2650         $bkt->btype('circ_history');
2651         $bkt->pub('f');
2652         $e->create_container_copy_bucket($bkt) or return $e->die_event;
2653     }
2654
2655     my $item = Fieldmapper::container::copy_bucket_item->new;
2656
2657     $item->bucket($bkt->id);
2658     $item->target_copy($self->copy->id);
2659     $item->pos($pos);
2660
2661     $e->create_container_copy_bucket_item($item) or return $e->die_event;
2662     $e->commit;
2663
2664     return undef;
2665 }
2666
2667
2668 sub make_trigger_events {
2669     my $self = shift;
2670     return unless $self->circ;
2671     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
2672     $ses->request('open-ils.trigger.event.autocreate', 'checkout', $self->circ, $self->circ_lib) if $self->is_checkout;
2673     $ses->request('open-ils.trigger.event.autocreate', 'checkin', $self->circ, $self->circ_lib) if $self->is_checkin;
2674     # ignore response
2675 }
2676
2677
2678
2679 sub checkin_handle_lost_now_found {
2680     my ($self, $bill_type) = @_;
2681
2682     # ------------------------------------------------------------------
2683     # remove charge from patron's account if lost item is returned
2684     # ------------------------------------------------------------------
2685
2686     my $bills = $self->editor->search_money_billing(
2687         {
2688             xact => $self->circ->id,
2689             btype => $bill_type
2690         }
2691     );
2692
2693     $logger->debug("voiding lost item charge of  ".scalar(@$bills));
2694     for my $bill (@$bills) {
2695         if( !$U->is_true($bill->voided) ) {
2696             $logger->info("lost item returned - voiding bill ".$bill->id);
2697             $bill->voided('t');
2698             $bill->void_time('now');
2699             $bill->voider($self->editor->requestor->id);
2700             my $note = ($bill->note) ? $bill->note . "\n" : '';
2701             $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
2702
2703             $self->bail_on_events($self->editor->event)
2704                 unless $self->editor->update_money_billing($bill);
2705         }
2706     }
2707 }
2708
2709 sub checkin_handle_lost_now_found_restore_od {
2710     my $self = shift;
2711
2712     # ------------------------------------------------------------------
2713     # restore those overdue charges voided when item was set to lost
2714     # ------------------------------------------------------------------
2715
2716     my $ods = $self->editor->search_money_billing(
2717         {
2718                 xact => $self->circ->id,
2719                 btype => 1
2720         }
2721     );
2722
2723     $logger->debug("returning overdue charges pre-lost  ".scalar(@$ods));
2724     for my $bill (@$ods) {
2725         if( $U->is_true($bill->voided) ) {
2726                 $logger->info("lost item returned - restoring overdue ".$bill->id);
2727                 $bill->voided('f');
2728                 $bill->clear_void_time;
2729                 $bill->voider($self->editor->requestor->id);
2730                 my $note = ($bill->note) ? $bill->note . "\n" : '';
2731                 $bill->note("${note}System: LOST RETURNED - OVERDUES REINSTATED");
2732
2733                 $self->bail_on_events($self->editor->event)
2734                         unless $self->editor->update_money_billing($bill);
2735         }
2736     }
2737 }
2738
2739