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