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