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