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