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