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