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