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