]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
added org unit setting to force patron penalty checks on renewals
[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
14 sub initialize {
15
16     my $self = shift;
17     my $conf = OpenSRF::Utils::SettingsClient->new;
18     my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
19     my @pfx = ( @pfx2, "scripts" );
20
21     my $p       = $conf->config_value(  @pfx, 'circ_permit_patron' );
22     my $c       = $conf->config_value(  @pfx, 'circ_permit_copy' );
23     my $d       = $conf->config_value(  @pfx, 'circ_duration' );
24     my $f       = $conf->config_value(  @pfx, 'circ_recurring_fines' );
25     my $m       = $conf->config_value(  @pfx, 'circ_max_fines' );
26     my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
27     my $lb  = $conf->config_value(  @pfx2, 'script_path' );
28
29     $logger->error( "Missing circ script(s)" ) 
30         unless( $p and $c and $d and $f and $m and $pr );
31
32     $scripts{circ_permit_patron}    = $p;
33     $scripts{circ_permit_copy}      = $c;
34     $scripts{circ_duration}         = $d;
35     $scripts{circ_recurring_fines}= $f;
36     $scripts{circ_max_fines}        = $m;
37     $scripts{circ_permit_renew} = $pr;
38
39     $lb = [ $lb ] unless ref($lb);
40     $script_libs = $lb;
41
42     $logger->debug(
43         "circulator: Loaded rules scripts for circ: " .
44         "circ permit patron = $p, ".
45         "circ permit copy = $c, ".
46         "circ duration = $d, ".
47         "circ recurring fines = $f, " .
48         "circ max fines = $m, ".
49         "circ renew permit = $pr.  ".
50         "lib paths = @$lb");
51 }
52
53
54 __PACKAGE__->register_method(
55     method  => "run_method",
56     api_name    => "open-ils.circ.checkout.permit",
57     notes       => q/
58         Determines if the given checkout can occur
59         @param authtoken The login session key
60         @param params A trailing hash of named params including 
61             barcode : The copy barcode, 
62             patron : The patron the checkout is occurring for, 
63             renew : true or false - whether or not this is a renewal
64         @return The event that occurred during the permit check.  
65     /);
66
67
68 __PACKAGE__->register_method (
69     method      => 'run_method',
70     api_name        => 'open-ils.circ.checkout.permit.override',
71     signature   => q/@see open-ils.circ.checkout.permit/,
72 );
73
74
75 __PACKAGE__->register_method(
76     method  => "run_method",
77     api_name    => "open-ils.circ.checkout",
78     notes => q/
79         Checks out an item
80         @param authtoken The login session key
81         @param params A named hash of params including:
82             copy            The copy object
83             barcode     If no copy is provided, the copy is retrieved via barcode
84             copyid      If no copy or barcode is provide, the copy id will be use
85             patron      The patron's id
86             noncat      True if this is a circulation for a non-cataloted item
87             noncat_type The non-cataloged type id
88             noncat_circ_lib The location for the noncat circ.  
89             precat      The item has yet to be cataloged
90             dummy_title The temporary title of the pre-cataloded item
91             dummy_author The temporary authr of the pre-cataloded item
92                 Default is the home org of the staff member
93         @return The SUCCESS event on success, any other event depending on the error
94     /);
95
96 __PACKAGE__->register_method(
97     method  => "run_method",
98     api_name    => "open-ils.circ.checkin",
99     argc        => 2,
100     signature   => q/
101         Generic super-method for handling all copies
102         @param authtoken The login session key
103         @param params Hash of named parameters including:
104             barcode - The copy barcode
105             force       - If true, copies in bad statuses will be checked in and give good statuses
106             ...
107     /
108 );
109
110 __PACKAGE__->register_method(
111     method  => "run_method",
112     api_name    => "open-ils.circ.checkin.override",
113     signature   => q/@see open-ils.circ.checkin/
114 );
115
116 __PACKAGE__->register_method(
117     method  => "run_method",
118     api_name    => "open-ils.circ.renew.override",
119     signature   => q/@see open-ils.circ.renew/,
120 );
121
122
123 __PACKAGE__->register_method(
124     method  => "run_method",
125     api_name    => "open-ils.circ.renew",
126     notes       => <<"    NOTES");
127     PARAMS( authtoken, circ => circ_id );
128     open-ils.circ.renew(login_session, circ_object);
129     Renews the provided circulation.  login_session is the requestor of the
130     renewal and if the logged in user is not the same as circ->usr, then
131     the logged in user must have RENEW_CIRC permissions.
132     NOTES
133
134 __PACKAGE__->register_method(
135     method  => "run_method",
136     api_name    => "open-ils.circ.checkout.full");
137 __PACKAGE__->register_method(
138     method  => "run_method",
139     api_name    => "open-ils.circ.checkout.full.override");
140
141
142
143 sub run_method {
144     my( $self, $conn, $auth, $args ) = @_;
145     translate_legacy_args($args);
146     my $api = $self->api_name;
147
148     my $circulator = 
149         OpenILS::Application::Circ::Circulator->new($auth, %$args);
150
151     return circ_events($circulator) if $circulator->bail_out;
152
153     # --------------------------------------------------------------------------
154     # Go ahead and load the script runner to make sure we have all 
155     # of the objects we need
156     # --------------------------------------------------------------------------
157     $circulator->is_renewal(1) if $api =~ /renew/;
158     $circulator->is_checkin(1) if $api =~ /checkin/;
159     $circulator->check_penalty_on_renew(1) if
160         $circulator->is_renewal and $U->ou_ancestor_setting_value(
161             $circulator->editor->requestor->ws_ou, 'circ.renew.check_penalty', $circulator->editor);
162     $circulator->mk_script_runner;
163     return circ_events($circulator) if $circulator->bail_out;
164
165     $circulator->circ_permit_patron($scripts{circ_permit_patron});
166     $circulator->circ_permit_copy($scripts{circ_permit_copy});      
167     $circulator->circ_duration($scripts{circ_duration});             
168     $circulator->circ_permit_renew($scripts{circ_permit_renew});
169     
170     $circulator->override(1) if $api =~ /override/o;
171
172     if( $api =~ /checkout\.permit/ ) {
173         $circulator->do_permit();
174
175     } elsif( $api =~ /checkout.full/ ) {
176
177         $circulator->do_permit();
178         unless( $circulator->bail_out ) {
179             $circulator->events([]);
180             $circulator->do_checkout();
181         }
182
183     } elsif( $api =~ /checkout/ ) {
184         $circulator->do_checkout();
185
186     } elsif( $api =~ /checkin/ ) {
187         $circulator->do_checkin();
188
189     } elsif( $api =~ /renew/ ) {
190         $circulator->is_renewal(1);
191         $circulator->do_renew();
192     }
193
194     if( $circulator->bail_out ) {
195
196         my @ee;
197         # make sure no success event accidentally slip in
198         $circulator->events(
199             [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
200
201         # Log the events
202         my @e = @{$circulator->events};
203         push( @ee, $_->{textcode} ) for @e;
204         $logger->info("circulator: bailing out with events: @ee");
205
206         $circulator->editor->rollback;
207
208     } else {
209         $circulator->editor->commit;
210     }
211
212     $circulator->script_runner->cleanup;
213     
214     $conn->respond_complete(circ_events($circulator));
215
216     unless($circulator->bail_out) {
217         $circulator->do_hold_notify($circulator->notify_hold)
218             if $circulator->notify_hold;
219         $circulator->retarget_holds if $circulator->retarget;
220     }
221 }
222
223 sub circ_events {
224     my $circ = shift;
225     my @e = @{$circ->events};
226     # if we have multiple events, SUCCESS should not be one of them;
227     @e = grep { $_->{textcode} ne 'SUCCESS' } @e if @e > 1;
228     return (@e == 1) ? $e[0] : \@e;
229 }
230
231
232
233 sub translate_legacy_args {
234     my $args = shift;
235
236     if( $$args{barcode} ) {
237         $$args{copy_barcode} = $$args{barcode};
238         delete $$args{barcode};
239     }
240
241     if( $$args{copyid} ) {
242         $$args{copy_id} = $$args{copyid};
243         delete $$args{copyid};
244     }
245
246     if( $$args{patronid} ) {
247         $$args{patron_id} = $$args{patronid};
248         delete $$args{patronid};
249     }
250
251     if( $$args{patron} and !ref($$args{patron}) ) {
252         $$args{patron_id} = $$args{patron};
253         delete $$args{patron};
254     }
255
256
257     if( $$args{noncat} ) {
258         $$args{is_noncat} = $$args{noncat};
259         delete $$args{noncat};
260     }
261
262     if( $$args{precat} ) {
263         $$args{is_precat} = $$args{precat};
264         delete $$args{precat};
265     }
266
267 }
268
269
270
271 # --------------------------------------------------------------------------
272 # This package actually manages all of the circulation logic
273 # --------------------------------------------------------------------------
274 package OpenILS::Application::Circ::Circulator;
275 use strict; use warnings;
276 use vars q/$AUTOLOAD/;
277 use DateTime;
278 use OpenILS::Utils::Fieldmapper;
279 use OpenSRF::Utils::Cache;
280 use Digest::MD5 qw(md5_hex);
281 use DateTime::Format::ISO8601;
282 use OpenILS::Utils::PermitHold;
283 use OpenSRF::Utils qw/:datetime/;
284 use OpenSRF::Utils::SettingsClient;
285 use OpenILS::Application::Circ::Holds;
286 use OpenILS::Application::Circ::Transit;
287 use OpenSRF::Utils::Logger qw(:logger);
288 use OpenILS::Utils::CStoreEditor qw/:funcs/;
289 use OpenILS::Application::Circ::ScriptBuilder;
290 use OpenILS::Const qw/:const/;
291
292 my $holdcode    = "OpenILS::Application::Circ::Holds";
293 my $transcode   = "OpenILS::Application::Circ::Transit";
294
295 sub DESTROY { }
296
297
298 # --------------------------------------------------------------------------
299 # Add a pile of automagic getter/setter methods
300 # --------------------------------------------------------------------------
301 my @AUTOLOAD_FIELDS = qw/
302     notify_hold
303     penalty_request
304     remote_hold
305     backdate
306     copy
307     copy_id
308     copy_barcode
309     patron
310     patron_id
311     patron_barcode
312     script_runner
313     volume
314     title
315     is_renewal
316     check_penalty_on_renew
317     is_noncat
318     is_precat
319     is_checkin
320     noncat_type
321     editor
322     events
323     cache_handle
324     override
325     circ_permit_patron
326     circ_permit_copy
327     circ_duration
328     circ_recurring_fines
329     circ_max_fines
330     circ_permit_renew
331     circ
332     transit
333     hold
334     permit_key
335     noncat_circ_lib
336     noncat_count
337     checkout_time
338     dummy_title
339     dummy_author
340     circ_lib
341     barcode
342     duration_level
343     recurring_fines_level
344     duration_rule
345     recurring_fines_rule
346     max_fine_rule
347     renewal_remaining
348     due_date
349     fulfilled_holds
350     transit
351     checkin_changed
352     force
353     old_circ
354     permit_override
355     pending_checkouts
356     cancelled_hold_transit
357     opac_renewal
358     phone_renewal
359     desk_renewal
360     retarget
361 /;
362
363
364 sub AUTOLOAD {
365     my $self = shift;
366     my $type = ref($self) or die "$self is not an object";
367     my $data = shift;
368     my $name = $AUTOLOAD;
369     $name =~ s/.*://o;   
370
371     unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
372         $logger->error("circulator: $type: invalid autoload field: $name");
373         die "$type: invalid autoload field: $name\n" 
374     }
375
376     {
377         no strict 'refs';
378         *{"${type}::${name}"} = sub {
379             my $s = shift;
380             my $v = shift;
381             $s->{$name} = $v if defined $v;
382             return $s->{$name};
383         }
384     }
385     return $self->$name($data);
386 }
387
388
389 sub new {
390     my( $class, $auth, %args ) = @_;
391     $class = ref($class) || $class;
392     my $self = bless( {}, $class );
393
394     $self->events([]);
395     $self->editor( 
396         new_editor(xact => 1, authtoken => $auth) );
397
398     unless( $self->editor->checkauth ) {
399         $self->bail_on_events($self->editor->event);
400         return $self;
401     }
402
403     $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
404
405     $self->$_($args{$_}) for keys %args;
406
407     $self->circ_lib(
408         ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
409
410     # if this is a renewal, default to desk_renewal
411     $self->desk_renewal(1) unless 
412         $self->opac_renewal or $self->phone_renewal;
413
414     return $self;
415 }
416
417
418 # --------------------------------------------------------------------------
419 # True if we should discontinue processing
420 # --------------------------------------------------------------------------
421 sub bail_out {
422     my( $self, $bool ) = @_;
423     if( defined $bool ) {
424         $logger->info("circulator: BAILING OUT") if $bool;
425         $self->{bail_out} = $bool;
426     }
427     return $self->{bail_out};
428 }
429
430
431 sub push_events {
432     my( $self, @evts ) = @_;
433     for my $e (@evts) {
434         next unless $e;
435         $logger->info("circulator: pushing event ".$e->{textcode});
436         push( @{$self->events}, $e ) unless
437             grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
438     }
439 }
440
441 sub mk_permit_key {
442     my $self = shift;
443     my $key = md5_hex( time() . rand() . "$$" );
444     $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
445     return $self->permit_key($key);
446 }
447
448 sub check_permit_key {
449     my $self = shift;
450     my $key = $self->permit_key;
451     return 0 unless $key;
452     my $k = "oils_permit_key_$key";
453     my $one = $self->cache_handle->get_cache($k);
454     $self->cache_handle->delete_cache($k);
455     return ($one) ? 1 : 0;
456 }
457
458
459 # --------------------------------------------------------------------------
460 # This builds the script runner environment and fetches most of the
461 # objects we need
462 # --------------------------------------------------------------------------
463 sub mk_script_runner {
464     my $self = shift;
465     my $args = {};
466
467
468     my @fields = 
469         qw/copy copy_barcode copy_id patron 
470             patron_id patron_barcode volume title editor/;
471
472     # Translate our objects into the ScriptBuilder args hash
473     $$args{$_} = $self->$_() for @fields;
474
475     $args->{ignore_user_status} = 1 if $self->is_checkin;
476     $$args{fetch_patron_by_circ_copy} = 1;
477     $$args{fetch_patron_circ_info} = 1 unless $self->is_checkin;
478
479     if( my $pco = $self->pending_checkouts ) {
480         $logger->info("circulator: we were given a pending checkouts number of $pco");
481         $$args{patronItemsOut} = $pco;
482     }
483
484     # This fetches most of the objects we need
485     $self->script_runner(
486         OpenILS::Application::Circ::ScriptBuilder->build($args));
487
488     # Now we translate the ScriptBuilder objects back into self
489     $self->$_($$args{$_}) for @fields;
490
491     my @evts = @{$args->{_events}} if $args->{_events};
492
493     $logger->debug("circulator: script builder returned events: @evts") if @evts;
494
495
496     if(@evts) {
497         # Anything besides ASSET_COPY_NOT_FOUND will stop processing
498         if(!$self->is_noncat and 
499             @evts == 1 and 
500             $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
501                 $self->is_precat(1);
502
503         } else {
504             my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
505             return $self->bail_on_events(@e);
506         }
507     }
508
509     $self->is_precat(1) if $self->copy 
510         and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
511
512     # We can't renew if there is no copy
513     return $self->bail_on_events(@evts) if 
514         $self->is_renewal and !$self->copy;
515
516     # Set some circ-specific flags in the script environment
517     my $evt = "environment";
518     $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
519
520     if( $self->is_noncat ) {
521       $self->script_runner->insert("$evt.isNonCat", 1);
522       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
523     }
524
525     if( $self->is_precat ) {
526         $self->script_runner->insert("environment.isPrecat", 1, 1);
527     }
528
529     $self->script_runner->add_path( $_ ) for @$script_libs;
530
531     return 1;
532 }
533
534
535
536
537 # --------------------------------------------------------------------------
538 # Does the circ permit work
539 # --------------------------------------------------------------------------
540 sub do_permit {
541     my $self = shift;
542
543     $self->log_me("do_permit()");
544
545     unless( $self->editor->requestor->id == $self->patron->id ) {
546         return $self->bail_on_events($self->editor->event)
547             unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
548     }
549
550     $self->check_captured_holds();
551     $self->do_copy_checks();
552     return if $self->bail_out;
553     $self->run_patron_permit_scripts();
554     $self->run_copy_permit_scripts() 
555         unless $self->is_precat or $self->is_noncat;
556     $self->override_events() unless 
557         $self->is_renewal and not $self->check_penalty_on_renew;
558     return if $self->bail_out;
559
560     if( $self->is_precat ) {
561         $self->push_events(
562             OpenILS::Event->new(
563                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
564         return $self->bail_out(1) unless $self->is_renewal;
565     }
566
567     $self->push_events(
568       OpenILS::Event->new(
569             'SUCCESS', 
570             payload => $self->mk_permit_key));
571 }
572
573
574 sub check_captured_holds {
575    my $self    = shift;
576    my $copy    = $self->copy;
577    my $patron  = $self->patron;
578
579     return undef unless $copy;
580
581     my $s = $U->copy_status($copy->status)->id;
582     return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
583     $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
584
585     # Item is on the holds shelf, make sure it's going to the right person
586     my $holds   = $self->editor->search_action_hold_request(
587         [
588             { 
589                 current_copy        => $copy->id , 
590                 capture_time        => { '!=' => undef },
591                 cancel_time         => undef, 
592                 fulfillment_time    => undef 
593             },
594             { limit => 1 }
595         ]
596     );
597
598     if( $holds and $$holds[0] ) {
599         return undef if $$holds[0]->usr == $patron->id;
600     }
601
602     $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
603
604     $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
605 }
606
607
608 sub do_copy_checks {
609     my $self = shift;
610     my $copy = $self->copy;
611     return unless $copy;
612
613     my $stat = $U->copy_status($copy->status)->id;
614
615     # We cannot check out a copy if it is in-transit
616     if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
617         return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
618     }
619
620     $self->handle_claims_returned();
621     return if $self->bail_out;
622
623     # no claims returned circ was found, check if there is any open circ
624     unless( $self->is_renewal ) {
625         my $circs = $self->editor->search_action_circulation(
626             { target_copy => $copy->id, checkin_time => undef }
627         );
628
629         return $self->bail_on_events(
630             OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
631     }
632 }
633
634
635 sub send_penalty_request {
636     my $self = shift;
637     my $ses = OpenSRF::AppSession->create('open-ils.penalty');
638     $self->penalty_request(
639         $ses->request(  
640             'open-ils.penalty.patron_penalty.calculate', 
641             {   update => 1, 
642                 authtoken => $self->editor->authtoken,
643                 patron => $self->patron } ) );
644 }
645
646 sub gather_penalty_request {
647     my $self = shift;
648     return [] unless $self->penalty_request;
649     my $data = $self->penalty_request->recv;
650     if( ref $data ) {
651         throw $data if UNIVERSAL::isa($data,'Error');
652         $data = $data->content;
653         return $data->{fatal_penalties};
654     }
655     $logger->error("circulator: penalty request returned no data");
656     return [];
657 }
658
659 # ---------------------------------------------------------------------
660 # This pushes any patron-related events into the list but does not
661 # set bail_out for any events
662 # ---------------------------------------------------------------------
663 sub run_patron_permit_scripts {
664     my $self        = shift;
665     my $runner      = $self->script_runner;
666     my $patronid    = $self->patron->id;
667
668     $self->send_penalty_request() unless
669         $self->is_renewal and not $self->check_penalty_on_renew;
670
671
672     # ---------------------------------------------------------------------
673     # Now run the patron permit script 
674     # ---------------------------------------------------------------------
675     $runner->load($self->circ_permit_patron);
676     my $result = $runner->run or 
677         throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
678
679     my $patron_events = $result->{events};
680     my @allevents; 
681
682     my $penalties = ($self->is_renewal and not $self->check_penalty_on_renew) ? 
683         [] : $self->gather_penalty_request();
684
685     push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
686
687     $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
688
689     $self->push_events(@allevents);
690 }
691
692
693 sub run_copy_permit_scripts {
694     my $self = shift;
695     my $copy = $self->copy || return;
696     my $runner = $self->script_runner;
697     
698    # ---------------------------------------------------------------------
699    # Capture all of the copy permit events
700    # ---------------------------------------------------------------------
701    $runner->load($self->circ_permit_copy);
702    my $result = $runner->run or 
703         throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
704    my $copy_events = $result->{events};
705
706    # ---------------------------------------------------------------------
707    # Now collect all of the events together
708    # ---------------------------------------------------------------------
709     my @allevents;
710    push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
711
712     # See if this copy has an alert message
713     my $ae = $self->check_copy_alert();
714     push( @allevents, $ae ) if $ae;
715
716    # uniquify the events
717    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
718    @allevents = values %hash;
719
720    for (@allevents) {
721       $_->{payload} = $copy if 
722             ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
723    }
724
725     $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
726
727     $self->push_events(@allevents);
728 }
729
730
731 sub check_copy_alert {
732     my $self = shift;
733     return undef if $self->is_renewal;
734     return OpenILS::Event->new(
735         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
736         if $self->copy and $self->copy->alert_message;
737     return undef;
738 }
739
740
741
742 # --------------------------------------------------------------------------
743 # If the call is overriding and has permissions to override every collected
744 # event, the are cleared.  Any event that the caller does not have
745 # permission to override, will be left in the event list and bail_out will
746 # be set
747 # XXX We need code in here to cancel any holds/transits on copies 
748 # that are being force-checked out
749 # --------------------------------------------------------------------------
750 sub override_events {
751     my $self = shift;
752     my @events = @{$self->events};
753     return unless @events;
754
755     if(!$self->override) {
756         return $self->bail_out(1) 
757             if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
758     }   
759
760     $self->events([]);
761     
762    for my $e (@events) {
763       my $tc = $e->{textcode};
764       next if $tc eq 'SUCCESS';
765       my $ov = "$tc.override";
766       $logger->info("circulator: attempting to override event: $ov");
767
768         return $self->bail_on_events($self->editor->event)
769             unless( $self->editor->allowed($ov) );
770    }
771 }
772     
773
774 # --------------------------------------------------------------------------
775 # If there is an open claimsreturn circ on the requested copy, close the 
776 # circ if overriding, otherwise bail out
777 # --------------------------------------------------------------------------
778 sub handle_claims_returned {
779     my $self = shift;
780     my $copy = $self->copy;
781
782     my $CR = $self->editor->search_action_circulation(
783         {   
784             target_copy     => $copy->id,
785             stop_fines      => OILS_STOP_FINES_CLAIMSRETURNED,
786             checkin_time    => undef,
787         }
788     );
789
790     return unless ($CR = $CR->[0]); 
791
792     my $evt;
793
794     # - If the caller has set the override flag, we will check the item in
795     if($self->override) {
796
797         $CR->checkin_time('now');   
798         $CR->checkin_lib($self->editor->requestor->ws_ou);
799         $CR->checkin_staff($self->editor->requestor->id);
800
801         $evt = $self->editor->event 
802             unless $self->editor->update_action_circulation($CR);
803
804     } else {
805         $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
806     }
807
808     $self->bail_on_events($evt) if $evt;
809     return;
810 }
811
812
813 # --------------------------------------------------------------------------
814 # This performs the checkout
815 # --------------------------------------------------------------------------
816 sub do_checkout {
817     my $self = shift;
818
819     $self->log_me("do_checkout()");
820
821     # make sure perms are good if this isn't a renewal
822     unless( $self->is_renewal ) {
823         return $self->bail_on_events($self->editor->event)
824             unless( $self->editor->allowed('COPY_CHECKOUT') );
825     }
826
827     # verify the permit key
828     unless( $self->check_permit_key ) {
829         if( $self->permit_override ) {
830             return $self->bail_on_events($self->editor->event)
831                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
832         } else {
833             return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
834         }   
835     }
836
837     # if this is a non-cataloged circ, build the circ and finish
838     if( $self->is_noncat ) {
839         $self->checkout_noncat;
840         $self->push_events(
841             OpenILS::Event->new('SUCCESS', 
842             payload => { noncat_circ => $self->circ }));
843         return;
844     }
845
846     if( $self->is_precat ) {
847         $self->script_runner->insert("environment.isPrecat", 1, 1);
848         $self->make_precat_copy;
849         return if $self->bail_out;
850
851     } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
852         return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
853     }
854
855     $self->do_copy_checks;
856     return if $self->bail_out;
857
858     $self->run_checkout_scripts();
859     return if $self->bail_out;
860
861     $self->build_checkout_circ_object();
862     return if $self->bail_out;
863
864     $self->apply_modified_due_date();
865     return if $self->bail_out;
866
867     return $self->bail_on_events($self->editor->event)
868         unless $self->editor->create_action_circulation($self->circ);
869
870     # refresh the circ to force local time zone for now
871     $self->circ($self->editor->retrieve_action_circulation($self->circ->id));
872
873     $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
874     $self->update_copy;
875     return if $self->bail_out;
876
877     $self->handle_checkout_holds();
878     return if $self->bail_out;
879
880    # ------------------------------------------------------------------------------
881    # Update the patron penalty info in the DB.  Run it for permit-overrides or
882     # renewals since both of those cases do not require the penalty server to
883     # run during the permit phase of the checkout
884    # ------------------------------------------------------------------------------
885     if( $self->permit_override or $self->is_renewal ) {
886         $U->update_patron_penalties(
887             authtoken => $self->editor->authtoken,
888             patron    => $self->patron,
889             background  => 1,
890         );
891     }
892
893     my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
894     $self->push_events(
895         OpenILS::Event->new('SUCCESS',
896             payload  => {
897                 copy              => $U->unflesh_copy($self->copy),
898                 circ              => $self->circ,
899                 record            => $record,
900                 holds_fulfilled   => $self->fulfilled_holds,
901             }
902         )
903     );
904 }
905
906 sub update_copy {
907     my $self = shift;
908     my $copy = $self->copy;
909
910     my $stat = $copy->status if ref $copy->status;
911     my $loc = $copy->location if ref $copy->location;
912     my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
913
914     $copy->status($stat->id) if $stat;
915     $copy->location($loc->id) if $loc;
916     $copy->circ_lib($circ_lib->id) if $circ_lib;
917     $copy->editor($self->editor->requestor->id);
918     $copy->edit_date('now');
919     $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
920
921     return $self->bail_on_events($self->editor->event)
922         unless $self->editor->update_asset_copy($self->copy);
923
924     $copy->status($U->copy_status($copy->status));
925     $copy->location($loc) if $loc;
926     $copy->circ_lib($circ_lib) if $circ_lib;
927 }
928
929
930 sub bail_on_events {
931     my( $self, @evts ) = @_;
932     $self->push_events(@evts);
933     $self->bail_out(1);
934 }
935
936 sub handle_checkout_holds {
937    my $self    = shift;
938
939    my $copy    = $self->copy;
940    my $patron  = $self->patron;
941
942     my $holds   = $self->editor->search_action_hold_request(
943         { 
944             current_copy        => $copy->id , 
945             cancel_time         => undef, 
946             fulfillment_time    => undef 
947         }
948     );
949
950    my @fulfilled;
951
952    # XXX We should only fulfill one hold here...
953    # XXX If a hold was transited to the user who is checking out
954    # the item, we need to make sure that hold is what's grabbed
955    if(@$holds) {
956
957       # for now, just sort by id to get what should be the oldest hold
958       $holds = [ sort { $a->id <=> $b->id } @$holds ];
959       my @myholds = grep { $_->usr eq $patron->id } @$holds;
960       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
961
962       if(@myholds) {
963          my $hold = $myholds[0];
964
965          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
966
967          # if the hold was never officially captured, capture it.
968          $hold->capture_time('now') unless $hold->capture_time;
969
970             # just make sure it's set correctly
971          $hold->current_copy($copy->id); 
972
973          $hold->fulfillment_time('now');
974             $hold->fulfillment_staff($self->editor->requestor->id);
975             $hold->fulfillment_lib($self->editor->requestor->ws_ou);
976
977             return $self->bail_on_events($self->editor->event)
978                 unless $self->editor->update_action_hold_request($hold);
979
980             $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
981
982          push( @fulfilled, $hold->id );
983       }
984
985       # If there are any holds placed for other users that point to this copy,
986       # then we need to un-target those holds so the targeter can pick a new copy
987       for(@altholds) {
988
989          $logger->info("circulator: un-targeting hold ".$_->id.
990             " because copy ".$copy->id." is getting checked out");
991
992             # - make the targeter process this hold at next run
993          $_->clear_prev_check_time; 
994
995             # - clear out the targetted copy
996          $_->clear_current_copy;
997          $_->clear_capture_time;
998
999             return $self->bail_on_event($self->editor->event)
1000                 unless $self->editor->update_action_hold_request($_);
1001       }
1002    }
1003
1004     $self->fulfilled_holds(\@fulfilled);
1005 }
1006
1007
1008
1009 sub run_checkout_scripts {
1010     my $self = shift;
1011
1012     my $evt;
1013    my $runner = $self->script_runner;
1014    $runner->load($self->circ_duration);
1015
1016    my $result = $runner->run or 
1017         throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
1018
1019    my $duration   = $result->{durationRule};
1020    my $recurring  = $result->{recurringFinesRule};
1021    my $max_fine   = $result->{maxFine};
1022
1023     if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
1024
1025         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
1026         return $self->bail_on_events($evt) if $evt;
1027     
1028         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
1029         return $self->bail_on_events($evt) if $evt;
1030     
1031         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
1032         return $self->bail_on_events($evt) if $evt;
1033
1034     } else {
1035
1036         # The item circulates with an unlimited duration
1037         $duration   = undef;
1038         $recurring  = undef;
1039         $max_fine   = undef;
1040     }
1041
1042    $self->duration_rule($duration);
1043    $self->recurring_fines_rule($recurring);
1044    $self->max_fine_rule($max_fine);
1045 }
1046
1047
1048 sub build_checkout_circ_object {
1049     my $self = shift;
1050
1051    my $circ       = Fieldmapper::action::circulation->new;
1052    my $duration   = $self->duration_rule;
1053    my $max        = $self->max_fine_rule;
1054    my $recurring  = $self->recurring_fines_rule;
1055    my $copy       = $self->copy;
1056    my $patron     = $self->patron;
1057
1058     if( $duration ) {
1059
1060         my $dname = $duration->name;
1061         my $mname = $max->name;
1062         my $rname = $recurring->name;
1063     
1064         $logger->debug("circulator: building circulation ".
1065             "with duration=$dname, maxfine=$mname, recurring=$rname");
1066     
1067         $circ->duration( $duration->shrt ) 
1068             if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1069         $circ->duration( $duration->normal ) 
1070             if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1071         $circ->duration( $duration->extended ) 
1072             if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1073     
1074         $circ->recuring_fine( $recurring->low ) 
1075             if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1076         $circ->recuring_fine( $recurring->normal ) 
1077             if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1078         $circ->recuring_fine( $recurring->high ) 
1079             if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1080
1081         $circ->duration_rule( $duration->name );
1082         $circ->recuring_fine_rule( $recurring->name );
1083         $circ->max_fine_rule( $max->name );
1084         $circ->max_fine( $max->amount );
1085
1086         $circ->fine_interval($recurring->recurance_interval);
1087         $circ->renewal_remaining( $duration->max_renewals );
1088
1089     } else {
1090
1091         $logger->info("circulator: copy found with an unlimited circ duration");
1092         $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1093         $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1094         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1095         $circ->renewal_remaining(0);
1096     }
1097
1098    $circ->target_copy( $copy->id );
1099    $circ->usr( $patron->id );
1100    $circ->circ_lib( $self->circ_lib );
1101
1102    if( $self->is_renewal ) {
1103       $circ->opac_renewal('t') if $self->opac_renewal;
1104       $circ->phone_renewal('t') if $self->phone_renewal;
1105       $circ->desk_renewal('t') if $self->desk_renewal;
1106       $circ->renewal_remaining($self->renewal_remaining);
1107       $circ->circ_staff($self->editor->requestor->id);
1108    }
1109
1110
1111    # if the user provided an overiding checkout time,
1112    # (e.g. the checkout really happened several hours ago), then
1113    # we apply that here.  Does this need a perm??
1114     $circ->xact_start(clense_ISO8601($self->checkout_time))
1115         if $self->checkout_time;
1116
1117    # if a patron is renewing, 'requestor' will be the patron
1118    $circ->circ_staff($self->editor->requestor->id);
1119     $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1120
1121     $self->circ($circ);
1122 }
1123
1124
1125 sub apply_modified_due_date {
1126     my $self = shift;
1127     my $circ = $self->circ;
1128     my $copy = $self->copy;
1129
1130    if( $self->due_date ) {
1131
1132         return $self->bail_on_events($self->editor->event)
1133             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1134
1135       $circ->due_date(clense_ISO8601($self->due_date));
1136
1137    } else {
1138
1139       # if the due_date lands on a day when the location is closed
1140       return unless $copy and $circ->due_date;
1141
1142         #my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1143
1144         # due-date overlap should be determined by the location the item
1145         # is checked out from, not the owning or circ lib of the item
1146         my $org = $self->editor->requestor->ws_ou;
1147
1148       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1149             " with an item due date of ".$circ->due_date );
1150
1151       my $dateinfo = $U->storagereq(
1152          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1153             $org, $circ->due_date );
1154
1155       if($dateinfo) {
1156          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1157             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1158
1159             # XXX make the behavior more dynamic
1160             # for now, we just push the due date to after the close date
1161             $circ->due_date($dateinfo->{end});
1162       }
1163    }
1164 }
1165
1166
1167
1168 sub create_due_date {
1169     my( $self, $duration ) = @_;
1170    my ($sec,$min,$hour,$mday,$mon,$year) =
1171       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1172    $year += 1900; $mon += 1;
1173    my $due_date = sprintf(
1174       '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
1175       $year, $mon, $mday, $hour, $min, $sec);
1176    return $due_date;
1177 }
1178
1179
1180
1181 sub make_precat_copy {
1182     my $self = shift;
1183     my $copy = $self->copy;
1184
1185    if($copy) {
1186       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1187
1188       $copy->editor($self->editor->requestor->id);
1189       $copy->edit_date('now');
1190       $copy->dummy_title($self->dummy_title);
1191       $copy->dummy_author($self->dummy_author);
1192
1193         $self->update_copy();
1194         return;
1195    }
1196
1197    $logger->info("circulator: Creating a new precataloged ".
1198         "copy in checkout with barcode " . $self->copy_barcode);
1199
1200    $copy = Fieldmapper::asset::copy->new;
1201    $copy->circ_lib($self->circ_lib);
1202    $copy->creator($self->editor->requestor->id);
1203    $copy->editor($self->editor->requestor->id);
1204    $copy->barcode($self->copy_barcode);
1205    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1206    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1207    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1208
1209    $copy->dummy_title($self->dummy_title || "");
1210    $copy->dummy_author($self->dummy_author || "");
1211
1212     unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1213         $self->bail_out(1);
1214         $self->push_events($self->editor->event);
1215         return;
1216     }   
1217
1218     # this is a little bit of a hack, but we need to 
1219     # get the copy into the script runner
1220     $self->script_runner->insert("environment.copy", $copy, 1);
1221 }
1222
1223
1224 sub checkout_noncat {
1225     my $self = shift;
1226
1227     my $circ;
1228     my $evt;
1229
1230    my $lib      = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1231    my $count    = $self->noncat_count || 1;
1232    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1233
1234    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1235
1236    for(1..$count) {
1237
1238       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1239          $self->editor->requestor->id, 
1240             $self->patron->id, 
1241             $lib, 
1242             $self->noncat_type, 
1243             $cotime,
1244             $self->editor );
1245
1246         if( $evt ) {
1247             $self->push_events($evt);
1248             $self->bail_out(1);
1249             return; 
1250         }
1251         $self->circ($circ);
1252    }
1253 }
1254
1255
1256 sub do_checkin {
1257     my $self = shift;
1258     $self->log_me("do_checkin()");
1259
1260
1261     return $self->bail_on_events(
1262         OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1263         unless $self->copy;
1264
1265     if( $self->checkin_check_holds_shelf() ) {
1266         $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1267         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1268         $self->checkin_flesh_events;
1269         return;
1270     }
1271
1272     unless( $self->is_renewal ) {
1273         return $self->bail_on_events($self->editor->event)
1274             unless $self->editor->allowed('COPY_CHECKIN');
1275     }
1276
1277     $self->push_events($self->check_copy_alert());
1278     $self->push_events($self->check_checkin_copy_status());
1279
1280     # the renew code will have already found our circulation object
1281     unless( $self->is_renewal and $self->circ ) {
1282         my $circs = $self->editor->search_action_circulation(
1283             { target_copy => $self->copy->id, checkin_time => undef });
1284         $self->circ($$circs[0]);
1285
1286         # for now, just warn if there are multiple open circs on a copy
1287         $logger->warn("circulator: we have ".scalar(@$circs).
1288             " open circs for copy " .$self->copy->id."!!") if @$circs > 1;
1289     }
1290
1291     # if the circ is marked as 'claims returned', add the event to the list
1292     $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1293         if ($self->circ and $self->circ->stop_fines 
1294                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1295
1296     # handle the overridable events 
1297     $self->override_events unless $self->is_renewal;
1298     return if $self->bail_out;
1299     
1300     if( $self->copy ) {
1301         $self->transit(
1302             $self->editor->search_action_transit_copy(
1303             { target_copy => $self->copy->id, dest_recv_time => undef })->[0]); 
1304     }
1305
1306     if( $self->circ ) {
1307         $self->checkin_handle_circ;
1308         return if $self->bail_out;
1309         $self->checkin_changed(1);
1310
1311     } elsif( $self->transit ) {
1312         my $hold_transit = $self->process_received_transit;
1313         $self->checkin_changed(1);
1314
1315         if( $self->bail_out ) { 
1316             $self->checkin_flesh_events;
1317             return;
1318         }
1319         
1320         if( my $e = $self->check_checkin_copy_status() ) {
1321             # If the original copy status is special, alert the caller
1322             my $ev = $self->events;
1323             $self->events([$e]);
1324             $self->override_events;
1325             return if $self->bail_out;
1326             $self->events($ev);
1327         }
1328
1329         if( $hold_transit or 
1330                 $U->copy_status($self->copy->status)->id 
1331                     == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1332
1333          my $hold;
1334          if( $hold_transit ) {
1335             $hold = $self->editor->retrieve_action_hold_request($hold_transit->hold);
1336          } else {
1337                 ($hold) = $U->fetch_open_hold_by_copy($self->copy->id);
1338          }
1339
1340             $self->hold($hold);
1341
1342             if( $hold and $hold->cancel_time ) { # this transited hold was cancelled mid-transit
1343
1344                 $logger->info("circulator: we received a transit on a cancelled hold " . $hold->id);
1345                 $self->reshelve_copy(1);
1346                 $self->cancelled_hold_transit(1);
1347                 $self->notify_hold(0); # don't notify for cancelled holds
1348                 return if $self->bail_out;
1349
1350             } else {
1351
1352                 # hold transited to correct location
1353                 $self->checkin_flesh_events;
1354                 return;
1355             }
1356         } 
1357
1358     } elsif( $U->copy_status($self->copy->status)->id == OILS_COPY_STATUS_IN_TRANSIT ) {
1359
1360         $logger->warn("circulator: we have a copy ".$self->copy->barcode.
1361             " that is in-transit, but there is no transit.. repairing");
1362         $self->reshelve_copy(1);
1363         return if $self->bail_out;
1364     }
1365
1366     if( $self->is_renewal ) {
1367         $self->push_events(OpenILS::Event->new('SUCCESS'));
1368         return;
1369     }
1370
1371    # ------------------------------------------------------------------------------
1372    # Circulations and transits are now closed where necessary.  Now go on to see if
1373    # this copy can fulfill a hold or needs to be routed to a different location
1374    # ------------------------------------------------------------------------------
1375
1376     if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1377         return if $self->bail_out;
1378
1379    } else { # not needed for a hold
1380
1381         my $circ_lib = (ref $self->copy->circ_lib) ? 
1382                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1383
1384         if( $self->remote_hold ) {
1385             $circ_lib = $self->remote_hold->pickup_lib;
1386             $logger->warn("circulator: Copy ".$self->copy->barcode.
1387                 " is on a remote hold's shelf, sending to $circ_lib");
1388         }
1389
1390         $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1391
1392       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1393
1394             $self->checkin_handle_precat();
1395             return if $self->bail_out;
1396
1397       } else {
1398
1399             my $bc = $self->copy->barcode;
1400             $logger->info("circulator: copy $bc at the wrong location, sending to $circ_lib");
1401             $self->checkin_build_copy_transit($circ_lib);
1402             return if $self->bail_out;
1403             $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1404       }
1405    }
1406
1407     $self->reshelve_copy;
1408     return if $self->bail_out;
1409
1410     unless($self->checkin_changed) {
1411
1412         $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1413         my $stat = $U->copy_status($self->copy->status)->id;
1414
1415         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1416          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1417         $self->bail_out(1); # no need to commit anything
1418
1419     } else {
1420
1421         $self->push_events(OpenILS::Event->new('SUCCESS')) 
1422             unless @{$self->events};
1423     }
1424
1425
1426    # ------------------------------------------------------------------------------
1427    # Update the patron penalty info in the DB
1428    # ------------------------------------------------------------------------------
1429    $U->update_patron_penalties(
1430       authtoken => $self->editor->authtoken,
1431       patron    => $self->patron,
1432       background  => 1 ) if $self->is_checkin;
1433
1434     $self->checkin_flesh_events;
1435     return;
1436 }
1437
1438 sub reshelve_copy {
1439    my $self    = shift;
1440    my $force   = $self->force || shift;
1441    my $copy    = $self->copy;
1442
1443    my $stat = $U->copy_status($copy->status)->id;
1444
1445    if($force || (
1446       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1447       $stat != OILS_COPY_STATUS_CATALOGING and
1448       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1449       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1450
1451         $copy->status( OILS_COPY_STATUS_RESHELVING );
1452             $self->update_copy;
1453             $self->checkin_changed(1);
1454     }
1455 }
1456
1457
1458 # Returns true if the item is at the current location
1459 # because it was transited there for a hold and the 
1460 # hold has not been fulfilled
1461 sub checkin_check_holds_shelf {
1462     my $self = shift;
1463     return 0 unless $self->copy;
1464
1465     return 0 unless 
1466         $U->copy_status($self->copy->status)->id ==
1467             OILS_COPY_STATUS_ON_HOLDS_SHELF;
1468
1469     # find the hold that put us on the holds shelf
1470     my $holds = $self->editor->search_action_hold_request(
1471         { 
1472             current_copy => $self->copy->id,
1473             capture_time => { '!=' => undef },
1474             fulfillment_time => undef,
1475             cancel_time => undef,
1476         }
1477     );
1478
1479     unless(@$holds) {
1480         $logger->warn("circulator: copy is on-holds-shelf, but there is no hold - reshelving");
1481         $self->reshelve_copy(1);
1482         return 0;
1483     }
1484
1485     my $hold = $$holds[0];
1486
1487     $logger->info("circulator: we found a captured, un-fulfilled hold [".
1488         $hold->id. "] for copy ".$self->copy->barcode);
1489
1490     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1491         $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1492         return 1;
1493     }
1494
1495     $logger->info("circulator: hold is not for here..");
1496     $self->remote_hold($hold);
1497     return 0;
1498 }
1499
1500
1501 sub checkin_handle_precat {
1502     my $self    = shift;
1503    my $copy    = $self->copy;
1504
1505    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1506       $copy->status(OILS_COPY_STATUS_CATALOGING);
1507         $self->update_copy();
1508         $self->checkin_changed(1);
1509         $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1510    }
1511 }
1512
1513
1514 sub checkin_build_copy_transit {
1515     my $self            = shift;
1516     my $dest            = shift;
1517     my $copy       = $self->copy;
1518    my $transit    = Fieldmapper::action::transit_copy->new;
1519
1520     #$dest  ||= (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib;
1521     $logger->info("circulator: transiting copy to $dest");
1522
1523    $transit->source($self->editor->requestor->ws_ou);
1524    $transit->dest($dest);
1525    $transit->target_copy($copy->id);
1526    $transit->source_send_time('now');
1527    $transit->copy_status( $U->copy_status($copy->status)->id );
1528
1529     $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1530
1531     return $self->bail_on_events($self->editor->event)
1532         unless $self->editor->create_action_transit_copy($transit);
1533
1534    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1535     $self->update_copy;
1536     $self->checkin_changed(1);
1537 }
1538
1539
1540 sub attempt_checkin_hold_capture {
1541     my $self = shift;
1542     my $copy = $self->copy;
1543
1544     # See if this copy can fulfill any holds
1545     my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold( 
1546         $self->editor, $copy, $self->editor->requestor );
1547
1548     if(!$hold) {
1549         $logger->debug("circulator: no potential permitted".
1550             "holds found for copy ".$copy->barcode);
1551         return undef;
1552     }
1553
1554     $self->retarget($retarget);
1555
1556     $logger->info("circulator: found permitted hold ".
1557         $hold->id . " for copy, capturing...");
1558
1559     $hold->current_copy($copy->id);
1560     $hold->capture_time('now');
1561
1562     # prevent DB errors caused by fetching 
1563     # holds from storage, and updating through cstore
1564     $hold->clear_fulfillment_time;
1565     $hold->clear_fulfillment_staff;
1566     $hold->clear_fulfillment_lib;
1567     $hold->clear_expire_time; 
1568     $hold->clear_cancel_time;
1569     $hold->clear_prev_check_time unless $hold->prev_check_time;
1570
1571     $self->bail_on_events($self->editor->event)
1572         unless $self->editor->update_action_hold_request($hold);
1573     $self->hold($hold);
1574     $self->checkin_changed(1);
1575
1576     return 1 if $self->bail_out;
1577
1578     if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1579
1580         # This hold was captured in the correct location
1581     $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1582         $self->push_events(OpenILS::Event->new('SUCCESS'));
1583
1584         #$self->do_hold_notify($hold->id);
1585         $self->notify_hold($hold->id);
1586
1587     } else {
1588     
1589         # Hold needs to be picked up elsewhere.  Build a hold
1590         # transit and route the item.
1591         $self->checkin_build_hold_transit();
1592     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1593         return 1 if $self->bail_out;
1594         $self->push_events(
1595             OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1596     }
1597
1598     # make sure we save the copy status
1599     $self->update_copy;
1600     return 1;
1601 }
1602
1603 sub do_hold_notify {
1604     my( $self, $holdid ) = @_;
1605
1606     $logger->info("circulator: running delayed hold notify process");
1607
1608 #   my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1609 #       hold_id => $holdid, editor => new_editor(requestor=>$self->editor->requestor));
1610
1611     my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1612         hold_id => $holdid, requestor => $self->editor->requestor);
1613
1614     $logger->debug("circulator: built hold notifier");
1615
1616     if(!$notifier->event) {
1617
1618         $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1619
1620         my $stat = $notifier->send_email_notify;
1621         if( $stat == '1' ) {
1622             $logger->info("ciculator: hold notify succeeded for hold $holdid");
1623             return;
1624         } 
1625
1626         $logger->warn("ciculator:  * hold notify failed for hold $holdid");
1627
1628     } else {
1629         $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1630     }
1631 }
1632
1633 sub retarget_holds {
1634     $logger->info("retargeting prev_check_time=null holds after opportunistic capture");
1635     my $ses = OpenSRF::AppSession->create('open-ils.storage');
1636     $ses->request('open-ils.storage.action.hold_request.copy_targeter');
1637     # no reason to wait for the return value
1638     return;
1639 }
1640
1641 sub checkin_build_hold_transit {
1642     my $self = shift;
1643
1644    my $copy = $self->copy;
1645    my $hold = $self->hold;
1646    my $trans = Fieldmapper::action::hold_transit_copy->new;
1647
1648     $logger->debug("circulator: building hold transit for ".$copy->barcode);
1649
1650    $trans->hold($hold->id);
1651    $trans->source($self->editor->requestor->ws_ou);
1652    $trans->dest($hold->pickup_lib);
1653    $trans->source_send_time("now");
1654    $trans->target_copy($copy->id);
1655
1656     # when the copy gets to its destination, it will recover
1657     # this status - put it onto the holds shelf
1658    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1659
1660     return $self->bail_on_events($self->editor->event)
1661         unless $self->editor->create_action_hold_transit_copy($trans);
1662 }
1663
1664
1665
1666 sub process_received_transit {
1667     my $self = shift;
1668     my $copy = $self->copy;
1669     my $copyid = $self->copy->id;
1670
1671     my $status_name = $U->copy_status($copy->status)->name;
1672     $logger->debug("circulator: attempting transit receive on ".
1673         "copy $copyid. Copy status is $status_name");
1674
1675     my $transit = $self->transit;
1676
1677     if( $transit->dest != $self->editor->requestor->ws_ou ) {
1678         # - this item is in-transit to a different location
1679
1680         my $tid = $transit->id; 
1681         my $loc = $self->editor->requestor->ws_ou;
1682         my $dest = $transit->dest;
1683
1684         $logger->info("circulator: Fowarding transit on copy which is destined ".
1685             "for a different location. transit=$tid, copy=$copyid, current ".
1686             "location=$loc, destination location=$dest");
1687
1688         my $evt = OpenILS::Event->new('ROUTE_ITEM', org => $dest, payload => {});
1689
1690         # grab the associated hold object if available
1691         my $ht = $self->editor->retrieve_action_hold_transit_copy($tid);
1692         $self->hold($self->editor->retrieve_action_hold_request($ht->hold)) if $ht;
1693
1694         return $self->bail_on_events($evt);
1695     }
1696
1697    # The transit is received, set the receive time
1698    $transit->dest_recv_time('now');
1699     $self->bail_on_events($self->editor->event)
1700         unless $self->editor->update_action_transit_copy($transit);
1701
1702     my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1703
1704    $logger->info("circulator: Recovering original copy status in transit: ".$transit->copy_status);
1705    $copy->status( $transit->copy_status );
1706     $self->update_copy();
1707     return if $self->bail_out;
1708
1709     my $ishold = 0;
1710     if($hold_transit) { 
1711         #$self->do_hold_notify($hold_transit->hold);
1712         $self->notify_hold($hold_transit->hold);
1713         $ishold = 1;
1714     }
1715
1716     $self->push_events( 
1717         OpenILS::Event->new(
1718         'SUCCESS', 
1719         ishold => $ishold,
1720       payload => { transit => $transit, holdtransit => $hold_transit } ));
1721
1722     return $hold_transit;
1723 }
1724
1725
1726 sub checkin_handle_circ {
1727    my $self = shift;
1728     $U->logmark;
1729
1730    my $circ = $self->circ;
1731    my $copy = $self->copy;
1732    my $evt;
1733    my $obt;
1734
1735    # backdate the circ if necessary
1736    if($self->backdate) {
1737         $self->checkin_handle_backdate;
1738         return if $self->bail_out;
1739    }
1740
1741    if(!$circ->stop_fines) {
1742       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1743       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1744       $circ->stop_fines_time('now') unless $self->backdate;
1745       $circ->stop_fines_time($self->backdate) if $self->backdate;
1746    }
1747
1748    # see if there are any fines owed on this circ.  if not, close it
1749     ($obt) = $U->fetch_mbts($circ->id, $self->editor);
1750    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1751
1752     $logger->debug("circulator: ".$obt->balance_owed." is owed on this circulation");
1753
1754    # Set the checkin vars since we have the item
1755     $circ->checkin_time( ($self->backdate) ? $self->backdate : 'now' );
1756
1757    $circ->checkin_staff($self->editor->requestor->id);
1758    $circ->checkin_lib($self->editor->requestor->ws_ou);
1759
1760     my $circ_lib = (ref $self->copy->circ_lib) ?  
1761         $self->copy->circ_lib->id : $self->copy->circ_lib;
1762     my $stat = $U->copy_status($self->copy->status)->id;
1763
1764     # If the item is lost/missing and it needs to be sent home, don't 
1765     # reshelve the copy, leave it lost/missing so the recipient will know
1766     if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1767         and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1768         $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1769
1770     } else {
1771         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1772         $self->update_copy;
1773     }
1774
1775     return $self->bail_on_events($self->editor->event)
1776         unless $self->editor->update_action_circulation($circ);
1777 }
1778
1779
1780 sub checkin_handle_backdate {
1781     my $self = shift;
1782
1783     my $bd = $self->backdate;
1784
1785     # ------------------------------------------------------------------
1786     # clean up the backdate for date comparison
1787     # we want any bills created on or after the backdate
1788     # ------------------------------------------------------------------
1789     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1790     #$bd = "${bd}T23:59:59";
1791
1792     my $bills = $self->editor->search_money_billing(
1793         { 
1794             billing_ts => { '>=' => $bd }, 
1795             xact => $self->circ->id, 
1796             billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1797         }
1798     );
1799
1800     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1801
1802     for my $bill (@$bills) {    
1803         unless( $U->is_true($bill->voided) ) {
1804             $logger->info("backdate voiding bill ".$bill->id);
1805             $bill->voided('t');
1806             $bill->void_time('now');
1807             $bill->voider($self->editor->requestor->id);
1808             my $n = $bill->note || "";
1809             $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1810
1811             $self->bail_on_events($self->editor->event)
1812                 unless $self->editor->update_money_billing($bill);
1813         }
1814     }
1815 }
1816
1817
1818
1819 =head
1820 # XXX Legacy version for Circ.pm support
1821 sub _checkin_handle_backdate {
1822    my( $class, $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1823
1824     my $bd = $backdate;
1825     $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1826     $bd = "${bd}T23:59:59";
1827
1828    my $bills = $session->request(
1829       "open-ils.storage.direct.money.billing.search_where.atomic",
1830         billing_ts => { '>=' => $bd }, 
1831         xact => $circ->id,
1832         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1833     )->gather(1);
1834
1835     $logger->debug("backdate found ".scalar(@$bills)." bills to void");
1836
1837    if($bills) {
1838       for my $bill (@$bills) {
1839             unless( $U->is_true($bill->voided) ) {
1840                 $logger->debug("voiding bill ".$bill->id);
1841                 $bill->voided('t');
1842                 $bill->void_time('now');
1843                 $bill->voider($requestor->id);
1844                 my $n = $bill->note || "";
1845                 $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1846                 my $s = $session->request(
1847                     "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1848                 return $U->DB_UPDATE_FAILED($bill) unless $s;
1849             }
1850         }
1851    }
1852
1853     return 100;
1854 }
1855 =cut
1856
1857
1858
1859
1860
1861
1862 sub find_patron_from_copy {
1863     my $self = shift;
1864     my $circs = $self->editor->search_action_circulation(
1865         { target_copy => $self->copy->id, checkin_time => undef });
1866     my $circ = $circs->[0];
1867     return unless $circ;
1868     my $u = $self->editor->retrieve_actor_user($circ->usr)
1869         or return $self->bail_on_events($self->editor->event);
1870     $self->patron($u);
1871 }
1872
1873 sub check_checkin_copy_status {
1874     my $self = shift;
1875    my $copy = $self->copy;
1876
1877    my $islost     = 0;
1878    my $ismissing  = 0;
1879    my $evt        = undef;
1880
1881    my $status = $U->copy_status($copy->status)->id;
1882
1883    return undef
1884       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1885             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1886             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1887             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1888             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1889             $status == OILS_COPY_STATUS_CATALOGING  ||
1890             $status == OILS_COPY_STATUS_RESHELVING );
1891
1892    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1893       if( $status == OILS_COPY_STATUS_LOST );
1894
1895    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1896       if( $status == OILS_COPY_STATUS_MISSING );
1897
1898    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1899 }
1900
1901
1902
1903 # --------------------------------------------------------------------------
1904 # On checkin, we need to return as many relevant objects as we can
1905 # --------------------------------------------------------------------------
1906 sub checkin_flesh_events {
1907     my $self = shift;
1908
1909     if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
1910         and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1911             $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1912     }
1913
1914
1915     for my $evt (@{$self->events}) {
1916
1917         my $payload          = {};
1918         $payload->{copy}     = $U->unflesh_copy($self->copy);
1919         $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1920         $payload->{circ}     = $self->circ;
1921         $payload->{transit}  = $self->transit;
1922         $payload->{cancelled_hold_transit} = 1 if $self->cancelled_hold_transit;
1923
1924         # $self->hold may or may not have been replaced with a 
1925         # valid hold after processing a cancelled hold
1926         $payload->{hold} = $self->hold unless (not $self->hold or $self->hold->cancel_time);
1927
1928         $evt->{payload} = $payload;
1929     }
1930 }
1931
1932 sub log_me {
1933     my( $self, $msg ) = @_;
1934     my $bc = ($self->copy) ? $self->copy->barcode :
1935         $self->barcode;
1936     $bc ||= "";
1937     my $usr = ($self->patron) ? $self->patron->id : "";
1938     $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1939         ", recipient=$usr, copy=$bc");
1940 }
1941
1942
1943 sub do_renew {
1944     my $self = shift;
1945     $self->log_me("do_renew()");
1946     $self->is_renewal(1);
1947
1948     # Make sure there is an open circ to renew that is not
1949     # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1950     my $circ = $self->editor->search_action_circulation(
1951             { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1952
1953     if(!$circ) {
1954         $circ = $self->editor->search_action_circulation(
1955             { 
1956                 target_copy => $self->copy->id, 
1957                 stop_fines => OILS_STOP_FINES_MAX_FINES,
1958                 checkin_time => undef
1959             } 
1960         )->[0];
1961     }
1962
1963     return $self->bail_on_events($self->editor->event) unless $circ;
1964
1965     # A user is not allowed to renew another user's items without permission
1966     unless( $circ->usr eq $self->editor->requestor->id ) {
1967         return $self->bail_on_events($self->editor->events)
1968             unless $self->editor->allowed('RENEW_CIRC', $circ->circ_lib);
1969     }   
1970
1971     $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1972         if $circ->renewal_remaining < 1;
1973
1974     # -----------------------------------------------------------------
1975
1976     $self->renewal_remaining( $circ->renewal_remaining - 1 );
1977     $self->circ($circ);
1978
1979     $self->run_renew_permit;
1980
1981     # Check the item in
1982     $self->do_checkin();
1983     return if $self->bail_out;
1984
1985     unless( $self->permit_override ) {
1986         $self->do_permit();
1987         return if $self->bail_out;
1988         $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1989         $self->remove_event('ITEM_NOT_CATALOGED');
1990     }   
1991
1992     $self->override_events;
1993     return if $self->bail_out;
1994
1995     $self->events([]);
1996     $self->do_checkout();
1997 }
1998
1999
2000 sub remove_event {
2001     my( $self, $evt ) = @_;
2002     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2003     $logger->debug("circulator: removing event from list: $evt");
2004     my @events = @{$self->events};
2005     $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
2006 }
2007
2008
2009 sub have_event {
2010     my( $self, $evt ) = @_;
2011     $evt = (ref $evt) ? $evt->{textcode} : $evt;
2012     return grep { $_->{textcode} eq $evt } @{$self->events};
2013 }
2014
2015
2016
2017 sub run_renew_permit {
2018     my $self = shift;
2019    my $runner = $self->script_runner;
2020
2021    $runner->load($self->circ_permit_renew);
2022    my $result = $runner->run or 
2023         throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2024    my $events = $result->{events};
2025
2026    $logger->activity("ciculator: circ_permit_renew for user ".
2027       $self->patron->id." returned events: @$events") if @$events;
2028
2029     $self->push_events(OpenILS::Event->new($_)) for @$events;
2030     
2031     $logger->debug("circulator: re-creating script runner to be safe");
2032     $self->mk_script_runner;
2033 }
2034
2035
2036
2037