40d7fe389fb989c37dd900a4ed15c702457ea008
[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
798         return $self->bail_on_events($self->editor->event)
799                 unless $self->editor->update_asset_copy($self->copy);
800
801         $copy->status($U->copy_status($copy->status));
802         $copy->location($loc) if $loc;
803         $copy->circ_lib($circ_lib) if $circ_lib;
804 }
805
806
807 sub bail_on_events {
808         my( $self, @evts ) = @_;
809         $self->push_events(@evts);
810         $self->bail_out(1);
811 }
812
813 sub handle_checkout_holds {
814    my $self    = shift;
815
816    my $copy    = $self->copy;
817    my $patron  = $self->patron;
818
819         my $holds       = $self->editor->search_action_hold_request(
820                 { 
821                         current_copy            => $copy->id , 
822                         cancel_time                     => undef, 
823                         fulfillment_time        => undef 
824                 }
825         );
826
827    my @fulfilled;
828
829    # XXX We should only fulfill one hold here...
830    # XXX If a hold was transited to the user who is checking out
831    # the item, we need to make sure that hold is what's grabbed
832    if(@$holds) {
833
834       # for now, just sort by id to get what should be the oldest hold
835       $holds = [ sort { $a->id <=> $b->id } @$holds ];
836       my @myholds = grep { $_->usr eq $patron->id } @$holds;
837       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
838
839       if(@myholds) {
840          my $hold = $myholds[0];
841
842          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
843
844          # if the hold was never officially captured, capture it.
845          $hold->capture_time('now') unless $hold->capture_time;
846
847                         # just make sure it's set correctly
848          $hold->current_copy($copy->id); 
849
850          $hold->fulfillment_time('now');
851                         $hold->fulfillment_staff($self->editor->requestor->id);
852                         $hold->fulfillment_lib($self->editor->requestor->ws_ou);
853
854                         return $self->bail_on_events($self->editor->event)
855                                 unless $self->editor->update_action_hold_request($hold);
856
857                         $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
858
859          push( @fulfilled, $hold->id );
860       }
861
862       # If there are any holds placed for other users that point to this copy,
863       # then we need to un-target those holds so the targeter can pick a new copy
864       for(@altholds) {
865
866          $logger->info("circulator: un-targeting hold ".$_->id.
867             " because copy ".$copy->id." is getting checked out");
868
869                         # - make the targeter process this hold at next run
870          $_->clear_prev_check_time; 
871
872                         # - clear out the targetted copy
873          $_->clear_current_copy;
874          $_->clear_capture_time;
875
876                         return $self->bail_on_event($self->editor->event)
877                                 unless $self->editor->update_action_hold_request($_);
878       }
879    }
880
881         $self->fulfilled_holds(\@fulfilled);
882 }
883
884
885
886 sub run_checkout_scripts {
887         my $self = shift;
888
889         my $evt;
890    my $runner = $self->script_runner;
891    $runner->load($self->circ_duration);
892
893    my $result = $runner->run or 
894                 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
895
896    my $duration   = $result->{durationRule};
897    my $recurring  = $result->{recurringFinesRule};
898    my $max_fine   = $result->{maxFine};
899
900         if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
901
902                 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
903                 return $self->bail_on_events($evt) if $evt;
904         
905                 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
906                 return $self->bail_on_events($evt) if $evt;
907         
908                 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
909                 return $self->bail_on_events($evt) if $evt;
910
911         } else {
912
913                 # The item circulates with an unlimited duration
914                 $duration       = undef;
915                 $recurring      = undef;
916                 $max_fine       = undef;
917         }
918
919    $self->duration_rule($duration);
920    $self->recurring_fines_rule($recurring);
921    $self->max_fine_rule($max_fine);
922 }
923
924
925 sub build_checkout_circ_object {
926         my $self = shift;
927
928    my $circ       = Fieldmapper::action::circulation->new;
929    my $duration   = $self->duration_rule;
930    my $max        = $self->max_fine_rule;
931    my $recurring  = $self->recurring_fines_rule;
932    my $copy       = $self->copy;
933    my $patron     = $self->patron;
934
935         if( $duration ) {
936
937                 my $dname = $duration->name;
938                 my $mname = $max->name;
939                 my $rname = $recurring->name;
940         
941                 $logger->debug("circulator: building circulation ".
942                         "with duration=$dname, maxfine=$mname, recurring=$rname");
943         
944                 $circ->duration( $duration->shrt ) 
945                         if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
946                 $circ->duration( $duration->normal ) 
947                         if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
948                 $circ->duration( $duration->extended ) 
949                         if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
950         
951                 $circ->recuring_fine( $recurring->low ) 
952                         if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
953                 $circ->recuring_fine( $recurring->normal ) 
954                         if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
955                 $circ->recuring_fine( $recurring->high ) 
956                         if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
957
958                 $circ->duration_rule( $duration->name );
959                 $circ->recuring_fine_rule( $recurring->name );
960                 $circ->max_fine_rule( $max->name );
961                 $circ->max_fine( $max->amount );
962
963                 $circ->fine_interval($recurring->recurance_interval);
964                 $circ->renewal_remaining( $duration->max_renewals );
965
966         } else {
967
968                 $logger->info("circulator: copy found with an unlimited circ duration");
969                 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
970                 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
971                 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
972                 $circ->renewal_remaining(0);
973         }
974
975    $circ->target_copy( $copy->id );
976    $circ->usr( $patron->id );
977    $circ->circ_lib( $self->circ_lib );
978
979    if( $self->is_renewal ) {
980       $circ->opac_renewal(1);
981       $circ->renewal_remaining($self->renewal_remaining);
982       $circ->circ_staff($self->editor->requestor->id);
983    }
984
985    # if the user provided an overiding checkout time,
986    # (e.g. the checkout really happened several hours ago), then
987    # we apply that here.  Does this need a perm??
988         $circ->xact_start(clense_ISO8601($self->checkout_time))
989                 if $self->checkout_time;
990
991    # if a patron is renewing, 'requestor' will be the patron
992    $circ->circ_staff($self->editor->requestor->id);
993         $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
994
995         $self->circ($circ);
996 }
997
998
999 sub apply_modified_due_date {
1000         my $self = shift;
1001         my $circ = $self->circ;
1002         my $copy = $self->copy;
1003
1004    if( $self->due_date ) {
1005
1006                 return $self->bail_on_events($self->editor->event)
1007                         unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1008
1009       $circ->due_date(clense_ISO8601($self->due_date));
1010
1011    } else {
1012
1013       # if the due_date lands on a day when the location is closed
1014       return unless $copy and $circ->due_date;
1015
1016                 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1017
1018       $logger->info("circ searching for closed date overlap on lib $org".
1019                         " with an item due date of ".$circ->due_date );
1020
1021       my $dateinfo = $U->storagereq(
1022          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1023                         $org, $circ->due_date );
1024
1025       if($dateinfo) {
1026          $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1027             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1028
1029             # XXX make the behavior more dynamic
1030             # for now, we just push the due date to after the close date
1031             $circ->due_date($dateinfo->{end});
1032       }
1033    }
1034 }
1035
1036
1037
1038 sub create_due_date {
1039         my( $self, $duration ) = @_;
1040    my ($sec,$min,$hour,$mday,$mon,$year) =
1041       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1042    $year += 1900; $mon += 1;
1043    my $due_date = sprintf(
1044       '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1045       $year, $mon, $mday, $hour, $min, $sec);
1046    return $due_date;
1047 }
1048
1049
1050
1051 sub make_precat_copy {
1052         my $self = shift;
1053         my $copy = $self->copy;
1054
1055    if($copy) {
1056       $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1057
1058       $copy->editor($self->editor->requestor->id);
1059       $copy->edit_date('now');
1060       $copy->dummy_title($self->dummy_title);
1061       $copy->dummy_author($self->dummy_author);
1062
1063                 $self->update_copy();
1064                 return;
1065    }
1066
1067    $logger->info("circulator: Creating a new precataloged ".
1068                 "copy in checkout with barcode " . $self->copy_barcode);
1069
1070    $copy = Fieldmapper::asset::copy->new;
1071    $copy->circ_lib($self->circ_lib);
1072    $copy->creator($self->editor->requestor->id);
1073    $copy->editor($self->editor->requestor->id);
1074    $copy->barcode($self->copy_barcode);
1075    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1076    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1077    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1078
1079    $copy->dummy_title($self->dummy_title || "");
1080    $copy->dummy_author($self->dummy_author || "");
1081
1082         unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1083                 $self->bail_out(1);
1084                 $self->push_events($self->editor->event);
1085                 return;
1086         }       
1087
1088         # this is a little bit of a hack, but we need to 
1089         # get the copy into the script runner
1090         $self->script_runner->insert("environment.copy", $copy, 1);
1091 }
1092
1093
1094 sub checkout_noncat {
1095         my $self = shift;
1096
1097         my $circ;
1098         my $evt;
1099
1100    my $lib              = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1101    my $count    = $self->noncat_count || 1;
1102    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1103
1104    $logger->info("circ creating $count noncat circs with checkout time $cotime");
1105
1106    for(1..$count) {
1107
1108       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1109          $self->editor->requestor->id, 
1110                         $self->patron->id, 
1111                         $lib, 
1112                         $self->noncat_type, 
1113                         $cotime,
1114                         $self->editor );
1115
1116                 if( $evt ) {
1117                         $self->push_events($evt);
1118                         $self->bail_out(1);
1119                         return; 
1120                 }
1121                 $self->circ($circ);
1122    }
1123 }
1124
1125
1126 sub do_checkin {
1127         my $self = shift;
1128         $self->log_me("do_checkin()");
1129
1130         return $self->bail_on_events(
1131                 OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1132                 unless $self->copy;
1133
1134         unless( $self->is_renewal ) {
1135                 return $self->bail_on_events($self->editor->event)
1136                         unless $self->editor->allowed('COPY_CHECKIN');
1137         }
1138
1139         $self->push_events($self->check_copy_alert());
1140         $self->push_events($self->check_checkin_copy_status());
1141
1142         # the renew code will have already found our circulation object
1143         unless( $self->is_renewal and $self->circ ) {
1144                 $self->circ(
1145                         $self->editor->search_action_circulation(
1146                         { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1147         }
1148
1149         # if the circ is marked as 'claims returned', add the event to the list
1150         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1151                 if ($self->circ and $self->circ->stop_fines 
1152                                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1153
1154         # handle the overridable events 
1155         $self->override_events unless $self->is_renewal;
1156         return if $self->bail_out;
1157         
1158         if( $self->copy ) {
1159                 $self->transit(
1160                         $self->editor->search_action_transit_copy(
1161                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1162         }
1163
1164         if( $self->circ ) {
1165                 $self->checkin_handle_circ;
1166                 return if $self->bail_out;
1167                 $self->checkin_changed(1);
1168
1169         } elsif( $self->transit ) {
1170                 my $hold_transit = $self->process_received_transit;
1171                 $self->checkin_changed(1);
1172
1173                 if( $self->bail_out ) { 
1174                         $self->checkin_flesh_events;
1175                         return;
1176                 }
1177                 
1178                 if( my $e = $self->check_checkin_copy_status() ) {
1179                         # If the original copy status is special, alert the caller
1180                         my $ev = $self->events;
1181                         $self->events([$e]);
1182                         $self->override_events;
1183                         return if $self->bail_out;
1184                         $self->events($ev);
1185                 }
1186
1187
1188                 if( $hold_transit or 
1189                                 $U->copy_status($self->copy->status)->id 
1190                                         == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1191                         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1192                         $self->checkin_flesh_events;
1193                         return;
1194                 } 
1195         }
1196
1197         if( $self->is_renewal ) {
1198                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1199                 return;
1200         }
1201
1202    # ------------------------------------------------------------------------------
1203    # Circulations and transits are now closed where necessary.  Now go on to see if
1204    # this copy can fulfill a hold or needs to be routed to a different location
1205    # ------------------------------------------------------------------------------
1206
1207         if( $self->attempt_checkin_hold_capture() ) {
1208                 return if $self->bail_out;
1209
1210    } else { # not needed for a hold
1211
1212                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1213                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1214
1215                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1216
1217       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1218
1219                         $self->checkin_handle_precat();
1220                         return if $self->bail_out;
1221
1222       } else {
1223
1224                         $self->checkin_build_copy_transit();
1225                         return if $self->bail_out;
1226                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1227       }
1228    }
1229
1230
1231         $self->reshelve_copy;
1232         return if $self->bail_out;
1233
1234         unless($self->checkin_changed) {
1235
1236                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1237                 my $stat = $U->copy_status($self->copy->status)->id;
1238
1239         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1240          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1241                 $self->bail_out(1); # no need to commit anything
1242
1243         } else {
1244                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1245                         unless @{$self->events};
1246         }
1247
1248         $self->checkin_flesh_events;
1249         return;
1250 }
1251
1252 sub reshelve_copy {
1253    my $self    = shift;
1254    my $force   = $self->force || shift;
1255    my $copy    = $self->copy;
1256
1257    my $stat = $U->copy_status($copy->status)->id;
1258
1259    if($force || (
1260       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1261       $stat != OILS_COPY_STATUS_CATALOGING and
1262       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1263       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1264
1265         $copy->status( OILS_COPY_STATUS_RESHELVING );
1266                         $self->update_copy;
1267                         $self->checkin_changed(1);
1268         }
1269 }
1270
1271
1272 sub checkin_handle_precat {
1273         my $self        = shift;
1274    my $copy    = $self->copy;
1275
1276    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1277       $copy->status(OILS_COPY_STATUS_CATALOGING);
1278                 $self->update_copy();
1279                 $self->checkin_changed(1);
1280                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1281    }
1282 }
1283
1284
1285 sub checkin_build_copy_transit {
1286         my $self                        = shift;
1287         my $copy       = $self->copy;
1288    my $transit    = Fieldmapper::action::transit_copy->new;
1289
1290    $transit->source($self->editor->requestor->ws_ou);
1291    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1292    $transit->target_copy($copy->id);
1293    $transit->source_send_time('now');
1294    $transit->copy_status( $U->copy_status($copy->status)->id );
1295
1296         $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1297
1298         return $self->bail_on_events($self->editor->event)
1299                 unless $self->editor->create_action_transit_copy($transit);
1300
1301    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1302         $self->update_copy;
1303         $self->checkin_changed(1);
1304 }
1305
1306
1307 sub attempt_checkin_hold_capture {
1308         my $self = shift;
1309         my $copy = $self->copy;
1310
1311         # See if this copy can fulfill any holds
1312         my ($hold) = $holdcode->find_nearest_permitted_hold(
1313                 OpenSRF::AppSession->create('open-ils.storage'), 
1314                 $copy, $self->editor->requestor );
1315
1316         if(!$hold) {
1317                 $logger->debug("circulator: no potential permitted".
1318                         "holds found for copy ".$copy->barcode);
1319                 return undef;
1320         }
1321
1322
1323         $logger->info("circulator: found permitted hold ".
1324                 $hold->id . " for copy, capturing...");
1325
1326         $hold->current_copy($copy->id);
1327         $hold->capture_time('now');
1328
1329         # prevent DB errors caused by fetching 
1330         # holds from storage, and updating through cstore
1331         $hold->clear_fulfillment_time;
1332         $hold->clear_fulfillment_staff;
1333         $hold->clear_fulfillment_lib;
1334         $hold->clear_expire_time; 
1335         $hold->clear_cancel_time;
1336         $hold->clear_prev_check_time unless $hold->prev_check_time;
1337
1338         $self->bail_on_events($self->editor->event)
1339                 unless $self->editor->update_action_hold_request($hold);
1340         $self->hold($hold);
1341         $self->checkin_changed(1);
1342
1343         return 1 if $self->bail_out;
1344
1345         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1346
1347                 # This hold was captured in the correct location
1348         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1349                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1350
1351                 $self->do_hold_notify($hold->id);
1352
1353         } else {
1354         
1355                 # Hold needs to be picked up elsewhere.  Build a hold
1356                 # transit and route the item.
1357                 $self->checkin_build_hold_transit();
1358         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1359                 return 1 if $self->bail_out;
1360                 $self->push_events(
1361                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1362         }
1363
1364         # make sure we save the copy status
1365         $self->update_copy;
1366         return 1;
1367 }
1368
1369 sub do_hold_notify {
1370         my( $self, $holdid ) = @_;
1371         my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1372                 editor => $self->editor, hold_id => $holdid );
1373
1374         if(!$notifier->event) {
1375
1376                 $logger->info("attempt at sending hold notification for hold $holdid");
1377
1378                 my $stat = $notifier->send_email_notify;
1379                 $logger->info("hold notify succeeded for hold $holdid") if $stat eq '1';
1380                 $logger->warn(" * hold notify failed for hold $holdid") if $stat ne '1';
1381
1382         } else {
1383                 $logger->info("Not sending hold notification since the patron has no email address");
1384         }
1385 }
1386
1387
1388 sub checkin_build_hold_transit {
1389         my $self = shift;
1390
1391
1392    my $copy = $self->copy;
1393    my $hold = $self->hold;
1394    my $trans = Fieldmapper::action::hold_transit_copy->new;
1395
1396         $logger->debug("circulator: building hold transit for ".$copy->barcode);
1397
1398    $trans->hold($hold->id);
1399    $trans->source($self->editor->requestor->ws_ou);
1400    $trans->dest($hold->pickup_lib);
1401    $trans->source_send_time("now");
1402    $trans->target_copy($copy->id);
1403
1404         # when the copy gets to its destination, it will recover
1405         # this status - put it onto the holds shelf
1406    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1407
1408         return $self->bail_on_events($self->editor->event)
1409                 unless $self->editor->create_action_hold_transit_copy($trans);
1410 }
1411
1412
1413
1414 sub process_received_transit {
1415         my $self = shift;
1416         my $copy = $self->copy;
1417    my $copyid = $self->copy->id;
1418
1419         my $status_name = $U->copy_status($copy->status)->name;
1420    $logger->debug("circulator: attempting transit receive on ".
1421                 "copy $copyid. Copy status is $status_name");
1422
1423         my $transit = $self->transit;
1424
1425    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1426       $logger->info("circulator: Fowarding transit on copy which is destined ".
1427          "for a different location. copy=$copyid,current ".
1428          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1429
1430                 return $self->bail_on_events(
1431                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1432    }
1433
1434    # The transit is received, set the receive time
1435    $transit->dest_recv_time('now');
1436         $self->bail_on_events($self->editor->event)
1437                 unless $self->editor->update_action_transit_copy($transit);
1438
1439         my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1440
1441    $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1442    $copy->status( $transit->copy_status );
1443         $self->update_copy();
1444         return if $self->bail_out;
1445
1446         my $ishold = 0;
1447         if($hold_transit) {     
1448                 $self->do_hold_notify($hold_transit->hold);
1449                 $ishold = 1;
1450         }
1451
1452         $self->push_events( 
1453                 OpenILS::Event->new(
1454                 'SUCCESS', 
1455                 ishold => $ishold,
1456       payload => { transit => $transit, holdtransit => $hold_transit } ));
1457
1458         return $hold_transit;
1459 }
1460
1461
1462 sub checkin_handle_circ {
1463    my $self = shift;
1464         $U->logmark;
1465
1466    my $circ = $self->circ;
1467    my $copy = $self->copy;
1468    my $evt;
1469    my $obt;
1470
1471    # backdate the circ if necessary
1472    if($self->backdate) {
1473                 $self->checkin_handle_backdate;
1474                 return if $self->bail_out;
1475    }
1476
1477    if(!$circ->stop_fines) {
1478       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1479       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1480       $circ->stop_fines_time('now');
1481    }
1482
1483    # see if there are any fines owed on this circ.  if not, close it
1484         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1485    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1486
1487    # Set the checkin vars since we have the item
1488    $circ->checkin_time('now');
1489    $circ->checkin_staff($self->editor->requestor->id);
1490    $circ->checkin_lib($self->editor->requestor->ws_ou);
1491
1492         my $circ_lib = (ref $self->copy->circ_lib) ?  
1493                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1494         my $stat = $U->copy_status($self->copy->status)->id;
1495
1496         # If the item is lost/missing and it needs to be sent home, don't 
1497         # reshelve the copy, leave it lost/missing so the recipient will know
1498         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1499                 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1500                 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1501
1502         } else {
1503                 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1504                 $self->update_copy;
1505         }
1506
1507
1508         return $self->bail_on_events($self->editor->event)
1509                 unless $self->editor->update_action_circulation($circ);
1510 }
1511
1512
1513 sub checkin_handle_backdate {
1514         my $self = shift;
1515
1516         my $bills = $self->editor->search_money_billing(
1517                 { 
1518                         billing_ts => { '>=' => $self->backdate }, 
1519                         xact => $self->circ->id, 
1520                         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1521                 }
1522         );
1523
1524         for my $bill (@$bills) {        
1525                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1526                         $bill->voided('t');
1527                         my $n = $bill->note || "";
1528                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1529
1530                         $self->bail_on_events($self->editor->event)
1531                                 unless $self->editor->update_money_billing($bill);
1532                 }
1533         }
1534 }
1535
1536
1537
1538 # XXX Legacy version for Circ.pm support
1539 sub _checkin_handle_backdate {
1540    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1541
1542    my $bills = $session->request(
1543       "open-ils.storage.direct.money.billing.search_where.atomic",
1544                 billing_ts => { '>=' => $backdate }, 
1545                 xact => $circ->id,
1546                 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1547         )->gather(1);
1548
1549    if($bills) {
1550       for my $bill (@$bills) {
1551          $bill->voided('t');
1552          my $n = $bill->note || "";
1553          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1554          my $s = $session->request(
1555             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1556          return $U->DB_UPDATE_FAILED($bill) unless $s;
1557       }
1558    }
1559 }
1560
1561
1562
1563
1564
1565
1566 sub find_patron_from_copy {
1567         my $self = shift;
1568         my $circs = $self->editor->search_action_circulation(
1569                 { target_copy => $self->copy->id, checkin_time => undef });
1570         my $circ = $circs->[0];
1571         return unless $circ;
1572         my $u = $self->editor->retrieve_actor_user($circ->usr)
1573                 or return $self->bail_on_events($self->editor->event);
1574         $self->patron($u);
1575 }
1576
1577 sub check_checkin_copy_status {
1578         my $self = shift;
1579    my $copy = $self->copy;
1580
1581    my $islost     = 0;
1582    my $ismissing  = 0;
1583    my $evt        = undef;
1584
1585    my $status = $U->copy_status($copy->status)->id;
1586
1587    return undef
1588       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1589             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1590             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1591             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1592             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1593             $status == OILS_COPY_STATUS_CATALOGING  ||
1594             $status == OILS_COPY_STATUS_RESHELVING );
1595
1596    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1597       if( $status == OILS_COPY_STATUS_LOST );
1598
1599    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1600       if( $status == OILS_COPY_STATUS_MISSING );
1601
1602    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1603 }
1604
1605
1606
1607 # --------------------------------------------------------------------------
1608 # On checkin, we need to return as many relevant objects as we can
1609 # --------------------------------------------------------------------------
1610 sub checkin_flesh_events {
1611         my $self = shift;
1612
1613         if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
1614                 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1615                         $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1616         }
1617
1618
1619         for my $evt (@{$self->events}) {
1620
1621                 my $payload          = {};
1622                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1623                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1624                 $payload->{circ}     = $self->circ;
1625                 $payload->{transit}  = $self->transit;
1626                 $payload->{hold}     = $self->hold;
1627                 
1628                 $evt->{payload} = $payload;
1629         }
1630 }
1631
1632 sub log_me {
1633         my( $self, $msg ) = @_;
1634         my $bc = ($self->copy) ? $self->copy->barcode :
1635                 $self->barcode;
1636         $bc ||= "";
1637         my $usr = ($self->patron) ? $self->patron->id : "";
1638         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1639                 ", recipient=$usr, copy=$bc");
1640 }
1641
1642
1643 sub do_renew {
1644         my $self = shift;
1645         $self->log_me("do_renew()");
1646         $self->is_renewal(1);
1647
1648         unless( $self->is_renewal ) {
1649                 return $self->bail_on_events($self->editor->events)
1650                         unless $self->editor->allowed('RENEW_CIRC');
1651         }       
1652
1653         # Make sure there is an open circ to renew that is not
1654         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1655         my $circ = $self->editor->search_action_circulation(
1656                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1657
1658         return $self->bail_on_events($self->editor->event) unless $circ;
1659
1660         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1661                 if $circ->renewal_remaining < 1;
1662
1663         # -----------------------------------------------------------------
1664
1665         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1666         $self->circ($circ);
1667
1668         $self->run_renew_permit;
1669
1670         # Check the item in
1671         $self->do_checkin();
1672         return if $self->bail_out;
1673
1674         unless( $self->permit_override ) {
1675                 $self->do_permit();
1676                 return if $self->bail_out;
1677                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1678                 $self->remove_event('ITEM_NOT_CATALOGED');
1679         }       
1680
1681         $self->override_events;
1682         return if $self->bail_out;
1683
1684         $self->events([]);
1685         $self->do_checkout();
1686 }
1687
1688
1689 sub remove_event {
1690         my( $self, $evt ) = @_;
1691         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1692         $logger->debug("circulator: removing event from list: $evt");
1693         my @events = @{$self->events};
1694         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1695 }
1696
1697
1698 sub have_event {
1699         my( $self, $evt ) = @_;
1700         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1701         return grep { $_->{textcode} eq $evt } @{$self->events};
1702 }
1703
1704
1705
1706 sub run_renew_permit {
1707         my $self = shift;
1708    my $runner = $self->script_runner;
1709
1710    $runner->load($self->circ_permit_renew);
1711    my $result = $runner->run or 
1712                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1713    my $events = $result->{events};
1714
1715    $logger->activity("circ_permit_renew for user ".
1716       $self->patron->id." returned events: @$events") if @$events;
1717
1718         $self->push_events(OpenILS::Event->new($_)) for @$events;
1719         
1720         $logger->debug("circulator: re-creating script runner to be safe");
1721         $self->mk_script_runner;
1722 }
1723
1724
1725
1726