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