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