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