fixed typo
[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         $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         # if the circ is marked as 'claims returned', add the event to the list
1146         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1147                 if ($self->circ and $self->circ->stop_fines 
1148                                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1149
1150         # handle the overridable events 
1151         $self->override_events unless $self->is_renewal;
1152         return if $self->bail_out;
1153         
1154         if( $self->copy ) {
1155                 $self->transit(
1156                         $self->editor->search_action_transit_copy(
1157                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1158         }
1159
1160         if( $self->circ ) {
1161                 $self->checkin_handle_circ;
1162                 return if $self->bail_out;
1163                 $self->checkin_changed(1);
1164
1165         } elsif( $self->transit ) {
1166                 my $hold_transit = $self->process_received_transit;
1167                 $self->checkin_changed(1);
1168
1169                 if( $self->bail_out ) { 
1170                         $self->checkin_flesh_events;
1171                         return;
1172                 }
1173                 
1174                 if( my $e = $self->check_checkin_copy_status() ) {
1175                         # If the original copy status is special, alert the caller
1176                         return $self->bail_on_events($e);       
1177                 }
1178
1179
1180                 if( $hold_transit or 
1181                                 $U->copy_status($self->copy->status)->id 
1182                                         == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1183                         $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1184                         $self->checkin_flesh_events;
1185                         return;
1186                 } 
1187         }
1188
1189         if( $self->is_renewal ) {
1190                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1191                 return;
1192         }
1193
1194    # ------------------------------------------------------------------------------
1195    # Circulations and transits are now closed where necessary.  Now go on to see if
1196    # this copy can fulfill a hold or needs to be routed to a different location
1197    # ------------------------------------------------------------------------------
1198
1199         if( $self->attempt_checkin_hold_capture() ) {
1200                 return if $self->bail_out;
1201
1202    } else { # not needed for a hold
1203
1204                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1205                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1206
1207                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1208
1209       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1210
1211                         $self->checkin_handle_precat();
1212                         return if $self->bail_out;
1213
1214       } else {
1215
1216                         $self->checkin_build_copy_transit();
1217                         return if $self->bail_out;
1218                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1219       }
1220    }
1221
1222
1223         $self->reshelve_copy;
1224         return if $self->bail_out;
1225
1226         unless($self->checkin_changed) {
1227
1228                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1229                 my $stat = $U->copy_status($self->copy->status)->id;
1230
1231         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1232          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1233                 $self->bail_out(1); # no need to commit anything
1234
1235         } else {
1236                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1237                         unless @{$self->events};
1238         }
1239
1240         $self->checkin_flesh_events;
1241         return;
1242 }
1243
1244 sub reshelve_copy {
1245    my $self    = shift;
1246    my $force   = $self->force || shift;
1247    my $copy    = $self->copy;
1248
1249    my $stat = $U->copy_status($copy->status)->id;
1250
1251    if($force || (
1252       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1253       $stat != OILS_COPY_STATUS_CATALOGING and
1254       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1255       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1256
1257         $copy->status( OILS_COPY_STATUS_RESHELVING );
1258                         $self->update_copy;
1259                         $self->checkin_changed(1);
1260         }
1261 }
1262
1263
1264 sub checkin_handle_precat {
1265         my $self        = shift;
1266    my $copy    = $self->copy;
1267
1268    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1269       $copy->status(OILS_COPY_STATUS_CATALOGING);
1270                 $self->update_copy();
1271                 $self->checkin_changed(1);
1272                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1273    }
1274 }
1275
1276
1277 sub checkin_build_copy_transit {
1278         my $self                        = shift;
1279         my $copy       = $self->copy;
1280    my $transit    = Fieldmapper::action::transit_copy->new;
1281
1282    $transit->source($self->editor->requestor->ws_ou);
1283    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1284    $transit->target_copy($copy->id);
1285    $transit->source_send_time('now');
1286    $transit->copy_status( $U->copy_status($copy->status)->id );
1287
1288         $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1289
1290         return $self->bail_on_events($self->editor->event)
1291                 unless $self->editor->create_action_transit_copy($transit);
1292
1293    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1294         $self->update_copy;
1295         $self->checkin_changed(1);
1296 }
1297
1298
1299 sub attempt_checkin_hold_capture {
1300         my $self = shift;
1301         my $copy = $self->copy;
1302
1303         # See if this copy can fulfill any holds
1304         my ($hold) = $holdcode->find_nearest_permitted_hold(
1305                 OpenSRF::AppSession->create('open-ils.storage'), 
1306                 $copy, $self->editor->requestor );
1307
1308         if(!$hold) {
1309                 $logger->debug("circulator: no potential permitted".
1310                         "holds found for copy ".$copy->barcode);
1311                 return undef;
1312         }
1313
1314
1315         $logger->info("circulator: found permitted hold ".
1316                 $hold->id . " for copy, capturing...");
1317
1318         $hold->current_copy($copy->id);
1319         $hold->capture_time('now');
1320
1321         # prevent DB errors caused by fetching 
1322         # holds from storage, and updating through cstore
1323         $hold->clear_fulfillment_time;
1324         $hold->clear_fulfillment_staff;
1325         $hold->clear_fulfillment_lib;
1326         $hold->clear_expire_time; 
1327         $hold->clear_cancel_time;
1328
1329         $self->bail_on_events($self->editor->event)
1330                 unless $self->editor->update_action_hold_request($hold);
1331         $self->hold($hold);
1332         $self->checkin_changed(1);
1333
1334         return 1 if $self->bail_out;
1335
1336         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1337
1338                 # This hold was captured in the correct location
1339         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1340                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1341
1342                 $self->do_hold_notify($hold->id);
1343
1344         } else {
1345         
1346                 # Hold needs to be picked up elsewhere.  Build a hold
1347                 # transit and route the item.
1348                 $self->checkin_build_hold_transit();
1349         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1350                 return 1 if $self->bail_out;
1351                 $self->push_events(
1352                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1353         }
1354
1355         # make sure we save the copy status
1356         $self->update_copy;
1357         return 1;
1358 }
1359
1360 sub do_hold_notify {
1361         my( $self, $holdid ) = @_;
1362         my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1363                 editor => $self->editor, hold_id => $holdid );
1364
1365         if(!$notifier->event) {
1366
1367                 $logger->info("attempt at sending hold notification for hold $holdid");
1368
1369                 my $stat = $notifier->send_email_notify;
1370                 $logger->info("hold notify succeeded for hold $holdid") if $stat eq '1';
1371                 $logger->warn(" * hold notify failed for hold $holdid") if $stat ne '1';
1372
1373         } else {
1374                 $logger->info("Not sending hold notification since the patron has no email address");
1375         }
1376 }
1377
1378
1379 sub checkin_build_hold_transit {
1380         my $self = shift;
1381
1382
1383    my $copy = $self->copy;
1384    my $hold = $self->hold;
1385    my $trans = Fieldmapper::action::hold_transit_copy->new;
1386
1387         $logger->debug("circulator: building hold transit for ".$copy->barcode);
1388
1389    $trans->hold($hold->id);
1390    $trans->source($self->editor->requestor->ws_ou);
1391    $trans->dest($hold->pickup_lib);
1392    $trans->source_send_time("now");
1393    $trans->target_copy($copy->id);
1394
1395         # when the copy gets to its destination, it will recover
1396         # this status - put it onto the holds shelf
1397    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1398
1399         return $self->bail_on_events($self->editor->event)
1400                 unless $self->editor->create_action_hold_transit_copy($trans);
1401 }
1402
1403
1404
1405 sub process_received_transit {
1406         my $self = shift;
1407         my $copy = $self->copy;
1408    my $copyid = $self->copy->id;
1409
1410         my $status_name = $U->copy_status($copy->status)->name;
1411    $logger->debug("circulator: attempting transit receive on ".
1412                 "copy $copyid. Copy status is $status_name");
1413
1414         my $transit = $self->transit;
1415
1416    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1417       $logger->info("circulator: Fowarding transit on copy which is destined ".
1418          "for a different location. copy=$copyid,current ".
1419          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1420
1421                 return $self->bail_on_events(
1422                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1423    }
1424
1425    # The transit is received, set the receive time
1426    $transit->dest_recv_time('now');
1427         $self->bail_on_events($self->editor->event)
1428                 unless $self->editor->update_action_transit_copy($transit);
1429
1430         my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1431
1432    $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1433    $copy->status( $transit->copy_status );
1434         $self->update_copy();
1435         return if $self->bail_out;
1436
1437         my $ishold = 0;
1438         if($hold_transit) {     
1439                 $self->do_hold_notify($hold_transit->hold);
1440                 $ishold = 1;
1441         }
1442
1443         $self->push_events( 
1444                 OpenILS::Event->new(
1445                 'SUCCESS', 
1446                 ishold => $ishold,
1447       payload => { transit => $transit, holdtransit => $hold_transit } ));
1448
1449         return $hold_transit;
1450 }
1451
1452
1453 sub checkin_handle_circ {
1454    my $self = shift;
1455         $U->logmark;
1456
1457    my $circ = $self->circ;
1458    my $copy = $self->copy;
1459    my $evt;
1460    my $obt;
1461
1462    # backdate the circ if necessary
1463    if($self->backdate) {
1464                 $self->checkin_handle_backdate;
1465                 return if $self->bail_out;
1466    }
1467
1468    if(!$circ->stop_fines) {
1469       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1470       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1471       $circ->stop_fines_time('now');
1472    }
1473
1474    # see if there are any fines owed on this circ.  if not, close it
1475         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1476    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1477
1478    # Set the checkin vars since we have the item
1479    $circ->checkin_time('now');
1480    $circ->checkin_staff($self->editor->requestor->id);
1481    $circ->checkin_lib($self->editor->requestor->ws_ou);
1482
1483         my $circ_lib = (ref $self->copy->circ_lib) ?  
1484                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1485         my $stat = $U->copy_status($self->copy->status)->id;
1486
1487         # If the item is lost/missing and it needs to be sent home, don't 
1488         # reshelve the copy, leave it lost/missing so the recipient will know
1489         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1490                 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1491                 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1492
1493         } else {
1494                 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1495                 $self->update_copy;
1496         }
1497
1498
1499         return $self->bail_on_events($self->editor->event)
1500                 unless $self->editor->update_action_circulation($circ);
1501 }
1502
1503
1504 sub checkin_handle_backdate {
1505         my $self = shift;
1506
1507         my $bills = $self->editor->search_money_billing(
1508                 { 
1509                         billing_ts => { '>=' => $self->backdate }, 
1510                         xact => $self->circ->id, 
1511                         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1512                 }
1513         );
1514
1515         for my $bill (@$bills) {        
1516                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1517                         $bill->voided('t');
1518                         my $n = $bill->note || "";
1519                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1520
1521                         $self->bail_on_events($self->editor->event)
1522                                 unless $self->editor->update_money_billing($bill);
1523                 }
1524         }
1525 }
1526
1527
1528
1529 # XXX Legacy version for Circ.pm support
1530 sub _checkin_handle_backdate {
1531    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1532
1533    my $bills = $session->request(
1534       "open-ils.storage.direct.money.billing.search_where.atomic",
1535                 billing_ts => { '>=' => $backdate }, 
1536                 xact => $circ->id,
1537                 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1538         )->gather(1);
1539
1540    if($bills) {
1541       for my $bill (@$bills) {
1542          $bill->voided('t');
1543          my $n = $bill->note || "";
1544          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1545          my $s = $session->request(
1546             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1547          return $U->DB_UPDATE_FAILED($bill) unless $s;
1548       }
1549    }
1550 }
1551
1552
1553
1554
1555
1556
1557 sub find_patron_from_copy {
1558         my $self = shift;
1559         my $circs = $self->editor->search_action_circulation(
1560                 { target_copy => $self->copy->id, checkin_time => undef });
1561         my $circ = $circs->[0];
1562         return unless $circ;
1563         my $u = $self->editor->retrieve_actor_user($circ->usr)
1564                 or return $self->bail_on_events($self->editor->event);
1565         $self->patron($u);
1566 }
1567
1568 sub check_checkin_copy_status {
1569         my $self = shift;
1570    my $copy = $self->copy;
1571
1572    my $islost     = 0;
1573    my $ismissing  = 0;
1574    my $evt        = undef;
1575
1576    my $status = $U->copy_status($copy->status)->id;
1577
1578    return undef
1579       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1580             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1581             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1582             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1583             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1584             $status == OILS_COPY_STATUS_CATALOGING  ||
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