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