]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
moved constants out to their own module, updating code
[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->mk_script_runner;
149         return circ_events($circulator) if $circulator->bail_out;
150
151         $circulator->circ_permit_patron($scripts{circ_permit_patron});
152         $circulator->circ_permit_copy($scripts{circ_permit_copy});              
153         $circulator->circ_duration($scripts{circ_duration});                     
154         $circulator->circ_permit_renew($scripts{circ_permit_renew});
155         
156         $circulator->override(1) if $api =~ /override/o;
157
158         if( $api =~ /checkout\.permit/ ) {
159                 $circulator->do_permit();
160
161         } elsif( $api =~ /checkout/ ) {
162                 $circulator->do_checkout();
163
164         } elsif( $api =~ /checkin/ ) {
165                 $circulator->do_checkin();
166
167         } elsif( $api =~ /renew/ ) {
168                 $circulator->is_renewal(1);
169                 $circulator->do_renew();
170         }
171
172         if( $circulator->bail_out ) {
173
174                 my @ee;
175                 # make sure no success event accidentally slip in
176                 $circulator->events(
177                         [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
178                 my @e = @{$circulator->events};
179                 push( @ee, $_->{textcode} ) for @e;
180                 $logger->info("circulator: bailing out with events: @ee");
181                 $circulator->editor->xact_rollback;
182
183         } else {
184                 $circulator->editor->commit;
185         }
186
187         $circulator->script_runner->cleanup;
188         
189         return circ_events($circulator);
190 }
191
192 sub circ_events {
193         my $circ = shift;
194         my @e = @{$circ->events};
195         return (@e == 1) ? $e[0] : \@e;
196 }
197
198
199
200 sub translate_legacy_args {
201         my $args = shift;
202
203         if( $$args{barcode} ) {
204                 $$args{copy_barcode} = $$args{barcode};
205                 delete $$args{barcode};
206         }
207
208         if( $$args{copyid} ) {
209                 $$args{copy_id} = $$args{copyid};
210                 delete $$args{copyid};
211         }
212
213         if( $$args{patronid} ) {
214                 $$args{patron_id} = $$args{patronid};
215                 delete $$args{patronid};
216         }
217
218         if( $$args{patron} and !ref($$args{patron}) ) {
219                 $$args{patron_id} = $$args{patron};
220                 delete $$args{patron};
221         }
222
223
224         if( $$args{noncat} ) {
225                 $$args{is_noncat} = $$args{noncat};
226                 delete $$args{noncat};
227         }
228
229         if( $$args{precat} ) {
230                 $$args{is_precat} = $$args{precat};
231                 delete $$args{precat};
232         }
233 }
234
235
236
237 # --------------------------------------------------------------------------
238 # This package actually manages all of the circulation logic
239 # --------------------------------------------------------------------------
240 package OpenILS::Application::Circ::Circulator;
241 use strict; use warnings;
242 use vars q/$AUTOLOAD/;
243 use DateTime;
244 use OpenILS::Utils::Fieldmapper;
245 use OpenSRF::Utils::Cache;
246 use Digest::MD5 qw(md5_hex);
247 use DateTime::Format::ISO8601;
248 use OpenILS::Utils::PermitHold;
249 use OpenSRF::Utils qw/:datetime/;
250 use OpenSRF::Utils::SettingsClient;
251 use OpenILS::Application::Circ::Holds;
252 use OpenILS::Application::Circ::Transit;
253 use OpenSRF::Utils::Logger qw(:logger);
254 use OpenILS::Utils::CStoreEditor qw/:funcs/;
255 use OpenILS::Application::Circ::ScriptBuilder;
256 use OpenILS::Const qw/:const/;
257
258 my $U                           = "OpenILS::Application::AppUtils";
259 my $holdcode    = "OpenILS::Application::Circ::Holds";
260 my $transcode   = "OpenILS::Application::Circ::Transit";
261
262 sub DESTROY { }
263
264
265 # --------------------------------------------------------------------------
266 # Add a pile of automagic getter/setter methods
267 # --------------------------------------------------------------------------
268 my @AUTOLOAD_FIELDS = qw/
269         backdate
270         copy
271         copy_id
272         copy_barcode
273         patron
274         patron_id
275         patron_barcode
276         script_runner
277         volume
278         title
279         is_renewal
280         is_noncat
281         is_precat
282         noncat_type
283         editor
284         events
285         cache_handle
286         override
287         circ_permit_patron
288         circ_permit_copy
289         circ_duration
290         circ_recurring_fines
291         circ_max_fines
292         circ_permit_renew
293         circ
294         transit
295         hold
296         permit_key
297         noncat_circ_lib
298         noncat_count
299         checkout_time
300         dummy_title
301         dummy_author
302         circ_lib
303         barcode
304    duration_level
305    recurring_fines_level
306    duration_rule
307    recurring_fines_rule
308    max_fine_rule
309         renewal_remaining
310         due_date
311         fulfilled_holds
312         transit
313         checkin_changed
314         force
315         old_circ
316         permit_override
317 /;
318
319
320 sub AUTOLOAD {
321         my $self = shift;
322         my $type = ref($self) or die "$self is not an object";
323         my $data = shift;
324         my $name = $AUTOLOAD;
325         $name =~ s/.*://o;   
326
327         unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
328                 $logger->error("$type: invalid autoload field: $name");
329                 die "$type: invalid autoload field: $name\n" 
330         }
331
332         {
333                 no strict 'refs';
334                 *{"${type}::${name}"} = sub {
335                         my $s = shift;
336                         my $v = shift;
337                         $s->{$name} = $v if defined $v;
338                         return $s->{$name};
339                 }
340         }
341         return $self->$name($data);
342 }
343
344
345 sub new {
346         my( $class, $auth, %args ) = @_;
347         $class = ref($class) || $class;
348         my $self = bless( {}, $class );
349
350         $self->events([]);
351         $self->editor( 
352                 new_editor(xact => 1, authtoken => $auth) );
353
354         unless( $self->editor->checkauth ) {
355                 $self->bail_on_events($self->editor->event);
356                 return $self;
357         }
358
359         $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
360
361         $self->$_($args{$_}) for keys %args;
362
363         $self->circ_lib(
364                 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
365
366         return $self;
367 }
368
369
370 # --------------------------------------------------------------------------
371 # True if we should discontinue processing
372 # --------------------------------------------------------------------------
373 sub bail_out {
374         my( $self, $bool ) = @_;
375         if( defined $bool ) {
376                 $logger->info("circulator: BAILING OUT") if $bool;
377                 $self->{bail_out} = $bool;
378         }
379         return $self->{bail_out};
380 }
381
382
383 sub push_events {
384         my( $self, @evts ) = @_;
385         for my $e (@evts) {
386                 next unless $e;
387                 $logger->info("circulator: pushing event ".$e->{textcode});
388                 push( @{$self->events}, $e ) unless
389                         grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
390         }
391 }
392
393 sub mk_permit_key {
394         my $self = shift;
395         my $key = md5_hex( time() . rand() . "$$" );
396         $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
397         return $self->permit_key($key);
398 }
399
400 sub check_permit_key {
401         my $self = shift;
402         my $key = $self->permit_key;
403         return 0 unless $key;
404         my $k = "oils_permit_key_$key";
405         my $one = $self->cache_handle->get_cache($k);
406         $self->cache_handle->delete_cache($k);
407         return ($one) ? 1 : 0;
408 }
409
410
411 # --------------------------------------------------------------------------
412 # This builds the script runner environment and fetches most of the
413 # objects we need
414 # --------------------------------------------------------------------------
415 sub mk_script_runner {
416         my $self = shift;
417         my $args = {};
418
419         my @fields = 
420                 qw/copy copy_barcode copy_id patron 
421                         patron_id patron_barcode volume title editor/;
422
423         # Translate our objects into the ScriptBuilder args hash
424         $$args{$_} = $self->$_() for @fields;
425         $$args{fetch_patron_by_circ_copy} = 1;
426         $$args{fetch_patron_circ_info} = 1;
427
428         # This fetches most of the objects we need
429         $self->script_runner(
430                 OpenILS::Application::Circ::ScriptBuilder->build($args));
431
432         # Now we translate the ScriptBuilder objects back into self
433         $self->$_($$args{$_}) for @fields;
434
435         my @evts = @{$args->{_events}} if $args->{_events};
436
437         $logger->debug("script builder returned events: : @evts") if @evts;
438
439
440         if(@evts) {
441                 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
442                 if(!$self->is_noncat and 
443                         @evts == 1 and 
444                         $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
445                                 $self->is_precat(1);
446
447                 } else {
448                         my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
449                         return $self->bail_on_events(@e);
450                 }
451         }
452
453         $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
454
455         # Set some circ-specific flags in the script environment
456         my $evt = "environment";
457         $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
458
459         if( $self->is_noncat ) {
460       $self->script_runner->insert("$evt.isNonCat", 1);
461       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
462         }
463
464         $self->script_runner->add_path( $_ ) for @$script_libs;
465
466         return 1;
467 }
468
469
470
471
472 # --------------------------------------------------------------------------
473 # Does the circ permit work
474 # --------------------------------------------------------------------------
475 sub do_permit {
476         my $self = shift;
477
478         $self->log_me("do_permit()");
479
480         unless( $self->editor->requestor->id == $self->patron->id ) {
481                 return $self->bail_on_events($self->editor->event)
482                         unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
483         }
484
485         $self->do_copy_checks();
486         return if $self->bail_out;
487         $self->run_patron_permit_scripts();
488         $self->run_copy_permit_scripts() 
489                 unless $self->is_precat or $self->is_noncat;
490         $self->override_events() unless $self->is_renewal;
491         return if $self->bail_out;
492
493         if( $self->is_precat ) {
494                 $self->push_events(
495                         OpenILS::Event->new(
496                                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
497                 return $self->bail_out(1) unless $self->is_renewal;
498         }
499
500         $self->push_events(
501       OpenILS::Event->new(
502                         'SUCCESS', 
503                         payload => $self->mk_permit_key));
504 }
505
506
507 sub do_copy_checks {
508         my $self = shift;
509         my $copy = $self->copy;
510         return unless $copy;
511
512         my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
513
514         # We cannot check out a copy if it is in-transit
515         if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
516                 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
517         }
518
519         $self->handle_claims_returned();
520         return if $self->bail_out;
521
522         # no claims returned circ was found, check if there is any open circ
523         unless( $self->is_renewal ) {
524                 my $circs = $self->editor->search_action_circulation(
525                         { target_copy => $copy->id, stop_fines_time => undef }
526                 );
527
528                 return $self->bail_on_events(
529                         OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
530         }
531 }
532
533
534 # ---------------------------------------------------------------------
535 # This pushes any patron-related events into the list but does not
536 # set bail_out for any events
537 # ---------------------------------------------------------------------
538 sub run_patron_permit_scripts {
539         my $self                = shift;
540         my $runner              = $self->script_runner;
541         my $patronid    = $self->patron->id;
542
543         # ---------------------------------------------------------------------
544         # Find all of the fatal penalties currently set on the user
545         # ---------------------------------------------------------------------
546         my $penalties = $U->update_patron_penalties( 
547                 authtoken => $self->editor->authtoken,
548                 patron    => $self->patron,
549         );
550
551         $penalties = $penalties->{fatal_penalties};
552
553         # ---------------------------------------------------------------------
554         # Now run the patron permit script 
555         # ---------------------------------------------------------------------
556         $runner->load($self->circ_permit_patron);
557         my $result = $runner->run or 
558                 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
559
560         my $patron_events = $result->{events};
561         my @allevents; 
562         push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
563
564         $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
565
566         $self->push_events(@allevents);
567 }
568
569
570 sub run_copy_permit_scripts {
571         my $self = shift;
572         my $copy = $self->copy || return;
573         my $runner = $self->script_runner;
574         
575    # ---------------------------------------------------------------------
576    # Capture all of the copy permit events
577    # ---------------------------------------------------------------------
578    $runner->load($self->circ_permit_copy);
579    my $result = $runner->run or 
580                 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
581    my $copy_events = $result->{events};
582
583    # ---------------------------------------------------------------------
584    # Now collect all of the events together
585    # ---------------------------------------------------------------------
586         my @allevents;
587    push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
588
589         # See if this copy has an alert message
590         my $ae = $self->check_copy_alert();
591         push( @allevents, $ae ) if $ae;
592
593    # uniquify the events
594    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
595    @allevents = values %hash;
596
597
598         # If the script says the copy is not available, put the status
599         # in as the payload for that event
600         my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
601    for (@allevents) {
602       $_->{payload} = $copy if 
603                         ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
604    }
605
606         $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
607
608         $self->push_events(@allevents);
609 }
610
611
612 sub check_copy_alert {
613         my $self = shift;
614         return OpenILS::Event->new(
615                 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
616                 if $self->copy and $self->copy->alert_message;
617         return undef;
618 }
619
620
621
622 # --------------------------------------------------------------------------
623 # If the call is overriding and has permissions to override every collected
624 # event, the are cleared.  Any event that the caller does not have
625 # permission to override, will be left in the event list and bail_out will
626 # be set
627 # XXX We need code in here to cancel any holds/transits on copies 
628 # that are being force-checked out
629 # --------------------------------------------------------------------------
630 sub override_events {
631         my $self = shift;
632         my @events = @{$self->events};
633         return unless @events;
634
635         if(!$self->override) {
636                 return $self->bail_out(1) 
637                         if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
638         }       
639
640         $self->events([]);
641         
642    for my $e (@events) {
643       my $tc = $e->{textcode};
644       next if $tc eq 'SUCCESS';
645       my $ov = "$tc.override";
646       $logger->info("circulator: attempting to override event: $ov");
647
648                 return $self->bail_on_events($self->editor->event)
649                         unless( $self->editor->allowed($ov)     );
650    }
651 }
652         
653
654 # --------------------------------------------------------------------------
655 # If there is an open claimsreturn circ on the requested copy, close the 
656 # circ if overriding, otherwise bail out
657 # --------------------------------------------------------------------------
658 sub handle_claims_returned {
659         my $self = shift;
660         my $copy = $self->copy;
661
662         my $CR = $self->editor->search_action_circulation(
663                 {       
664                         target_copy             => $copy->id,
665                         stop_fines              => OILS_STOP_FINES_CLAIMSRETURNED,
666                         checkin_time    => undef,
667                 }
668         );
669
670         return unless ($CR = $CR->[0]); 
671
672         my $evt;
673
674         # - If the caller has set the override flag, we will check the item in
675         if($self->override) {
676
677                 $CR->checkin_time('now');       
678                 $CR->checkin_lib($self->editor->requestor->ws_ou);
679                 $CR->checkin_staff($self->editor->requestor->id);
680
681                 $evt = $self->editor->event 
682                         unless $self->editor->update_action_circulation($CR);
683
684         } else {
685                 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
686         }
687
688         $self->bail_on_events($evt) if $evt;
689         return;
690 }
691
692
693 # --------------------------------------------------------------------------
694 # This performs the checkout
695 # --------------------------------------------------------------------------
696 sub do_checkout {
697         my $self = shift;
698
699         $self->log_me("do_checkout()");
700
701         # make sure perms are good if this isn't a renewal
702         unless( $self->is_renewal ) {
703                 return $self->bail_on_events($self->editor->event)
704                         unless( $self->editor->allowed('COPY_CHECKOUT') );
705         }
706
707         # verify the permit key
708         unless( $self->check_permit_key ) {
709                 if( $self->permit_override ) {
710                         return $self->bail_on_events($self->editor->event)
711                                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
712                 } else {
713                         return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
714                 }       
715         }
716
717         # if this is a non-cataloged circ, build the circ and finish
718         if( $self->is_noncat ) {
719                 $self->checkout_noncat;
720                 $self->push_events(
721                         OpenILS::Event->new('SUCCESS', 
722                         payload => { noncat_circ => $self->circ }));
723                 return;
724         }
725
726         if( $self->is_precat ) {
727                 $self->script_runner->insert("environment.isPrecat", 1, 1);
728                 $self->make_precat_copy;
729                 return if $self->bail_out;
730
731         } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
732                 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
733         }
734
735         $self->do_copy_checks;
736         return if $self->bail_out;
737
738         $self->run_checkout_scripts();
739         return if $self->bail_out;
740
741         $self->build_checkout_circ_object();
742         return if $self->bail_out;
743
744         $self->apply_modified_due_date();
745         return if $self->bail_out;
746
747         return $self->bail_on_events($self->editor->event)
748                 unless $self->editor->create_action_circulation($self->circ);
749
750         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
751         $self->update_copy;
752         return if $self->bail_out;
753
754         $self->handle_checkout_holds();
755         return if $self->bail_out;
756
757    # ------------------------------------------------------------------------------
758    # Update the patron penalty info in the DB
759    # ------------------------------------------------------------------------------
760    $U->update_patron_penalties(
761       authtoken => $self->editor->authtoken,
762       patron    => $self->patron,
763       background  => 1,
764    );
765
766         my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
767         $self->push_events(
768                 OpenILS::Event->new('SUCCESS',
769                         payload  => {
770                                 copy              => $U->unflesh_copy($self->copy),
771                                 circ              => $self->circ,
772                                 record            => $record,
773                                 holds_fulfilled   => $self->fulfilled_holds,
774                         }
775                 )
776         );
777 }
778
779 sub update_copy {
780         my $self = shift;
781         my $copy = $self->copy;
782
783         my $stat = $copy->status if ref $copy->status;
784         my $loc = $copy->location if ref $copy->location;
785         my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
786
787         $copy->status($stat->id) if $stat;
788         $copy->location($loc->id) if $loc;
789         $copy->circ_lib($circ_lib->id) if $circ_lib;
790
791         return $self->bail_on_events($self->editor->event)
792                 unless $self->editor->update_asset_copy($self->copy);
793
794         $copy->status($U->copy_status($copy->status));
795         $copy->location($loc) if $loc;
796         $copy->circ_lib($circ_lib) if $circ_lib;
797 }
798
799
800 sub bail_on_events {
801         my( $self, @evts ) = @_;
802         $self->push_events(@evts);
803         $self->bail_out(1);
804 }
805
806 sub handle_checkout_holds {
807    my $self    = shift;
808
809    my $copy    = $self->copy;
810    my $patron  = $self->patron;
811
812         my $holds       = $self->editor->search_action_hold_request(
813                 { 
814                         current_copy            =>  $copy->id , 
815                         cancel_time                     => undef, 
816                         fulfillment_time        => undef 
817                 }
818         );
819
820    my @fulfilled;
821
822    # XXX We should only fulfill one hold here...
823    # XXX If a hold was transited to the user who is checking out
824    # the item, we need to make sure that hold is what's grabbed
825    if(@$holds) {
826
827       # for now, just sort by id to get what should be the oldest hold
828       $holds = [ sort { $a->id <=> $b->id } @$holds ];
829       my @myholds = grep { $_->usr eq $patron->id } @$holds;
830       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
831
832       if(@myholds) {
833          my $hold = $myholds[0];
834
835          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
836
837          # if the hold was never officially captured, capture it.
838          $hold->capture_time('now') unless $hold->capture_time;
839
840                         # just make sure it's set correctly
841          $hold->current_copy($copy->id); 
842
843          $hold->fulfillment_time('now');
844                         $hold->fulfillment_staff($self->editor->requestor->id);
845                         $hold->fulfillment_lib($self->editor->requestor->ws_ou);
846
847                         return $self->bail_on_events($self->editor->event)
848                                 unless $self->editor->update_action_hold_request($hold);
849
850          push( @fulfilled, $hold->id );
851       }
852
853       # If there are any holds placed for other users that point to this copy,
854       # then we need to un-target those holds so the targeter can pick a new copy
855       for(@altholds) {
856
857          $logger->info("circulator: un-targeting hold ".$_->id.
858             " because copy ".$copy->id." is getting checked out");
859
860                         # - make the targeter process this hold at next run
861          $_->clear_prev_check_time; 
862
863                         # - clear out the targetted copy
864          $_->clear_current_copy;
865
866                         return $self->bail_on_event($self->editor->event)
867                                 unless $self->editor->update_action_hold_request($_);
868       }
869    }
870
871         $self->fulfilled_holds(\@fulfilled);
872 }
873
874
875
876 sub run_checkout_scripts {
877         my $self = shift;
878
879         my $evt;
880    my $runner = $self->script_runner;
881    $runner->load($self->circ_duration);
882
883    my $result = $runner->run or 
884                 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
885
886    my $duration   = $result->{durationRule};
887    my $dur_level  = $result->{durationLevel};
888    my $recurring  = $result->{recurringFinesRule};
889    my $max_fine   = $result->{maxFine};
890    my $rec_fines_level = $result->{recurringFinesLevel};
891
892    ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
893         return $self->bail_on_events($evt) if $evt;
894    ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
895         return $self->bail_on_events($evt) if $evt;
896    ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
897         return $self->bail_on_events($evt) if $evt;
898
899    $self->duration_level($dur_level);
900    $self->recurring_fines_level($rec_fines_level);
901    $self->duration_rule($duration);
902    $self->recurring_fines_rule($recurring);
903    $self->max_fine_rule($max_fine);
904 }
905
906
907 sub build_checkout_circ_object {
908         my $self = shift;
909
910    my $circ       = Fieldmapper::action::circulation->new;
911    my $duration   = $self->duration_rule;
912    my $max        = $self->max_fine_rule;
913    my $recurring  = $self->recurring_fines_rule;
914    my $copy       = $self->copy;
915    my $patron     = $self->patron;
916    my $dur_level  = $self->duration_level;
917    my $rec_level  = $self->recurring_fines_level;
918
919         my $dname = $duration->name;
920         my $mname = $max->name;
921         my $rname = $recurring->name;
922
923         $logger->debug("circulator: building circulation with duration=$dname, ".
924                 "maxfine=$mname, recurring=$rname, duration-level=$dur_level, recurring-level=$rec_level");
925
926         $circ->duration( $duration->shrt ) if ($dur_level == OILS_CIRC_DURATION_SHORT);
927    $circ->duration( $duration->normal ) if ($dur_level == OILS_CIRC_DURATION_NORMAL);
928    $circ->duration( $duration->extended ) if ($dur_level == OILS_CIRC_DURATION_EXTENDED);
929
930    $circ->recuring_fine( $recurring->low ) if ($rec_level eq OILS_REC_FINE_LEVEL_LOW);
931    $circ->recuring_fine( $recurring->normal ) if ($rec_level eq OILS_REC_FINE_LEVEL_NORMAL);
932    $circ->recuring_fine( $recurring->high ) if ($rec_level eq OILS_REC_FINE_LEVEL_HIGH);
933
934    $circ->duration_rule( $duration->name );
935    $circ->recuring_fine_rule( $recurring->name );
936    $circ->max_fine_rule( $max->name );
937    $circ->max_fine( $max->amount );
938
939    $circ->fine_interval($recurring->recurance_interval);
940    $circ->renewal_remaining( $duration->max_renewals );
941    $circ->target_copy( $copy->id );
942    $circ->usr( $patron->id );
943    $circ->circ_lib( $self->circ_lib );
944
945    if( $self->is_renewal ) {
946       $circ->opac_renewal(1);
947       $circ->renewal_remaining($self->renewal_remaining);
948       $circ->circ_staff($self->editor->requestor->id);
949    }
950
951
952    # if the user provided an overiding checkout time,
953    # (e.g. the checkout really happened several hours ago), then
954    # we apply that here.  Does this need a perm??
955         $circ->xact_start(clense_ISO8601($self->checkout_time))
956                 if $self->checkout_time;
957
958    # if a patron is renewing, 'requestor' will be the patron
959    $circ->circ_staff($self->editor->requestor->id);
960         $circ->due_date( $self->create_due_date($circ->duration) );
961
962         $self->circ($circ);
963 }
964
965
966 sub apply_modified_due_date {
967         my $self = shift;
968         my $circ = $self->circ;
969         my $copy = $self->copy;
970
971    if( $self->due_date ) {
972
973                 return $self->bail_on_events($self->editor->event)
974                         unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
975
976       $circ->due_date(clense_ISO8601($self->due_date));
977
978    } else {
979
980       # if the due_date lands on a day when the location is closed
981       return unless $copy;
982
983                 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
984
985       $logger->info("circ searching for closed date overlap on lib $org".
986                         " with an item due date of ".$circ->due_date );
987
988       my $dateinfo = $U->storagereq(
989          'open-ils.storage.actor.org_unit.closed_date.overlap', 
990                         $org, $circ->due_date );
991
992       if($dateinfo) {
993          $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
994             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
995
996             # XXX make the behavior more dynamic
997             # for now, we just push the due date to after the close date
998             $circ->due_date($dateinfo->{end});
999       }
1000    }
1001 }
1002
1003
1004
1005 sub create_due_date {
1006         my( $self, $duration ) = @_;
1007    my ($sec,$min,$hour,$mday,$mon,$year) =
1008       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1009    $year += 1900; $mon += 1;
1010    my $due_date = sprintf(
1011       '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1012       $year, $mon, $mday, $hour, $min, $sec);
1013    return $due_date;
1014 }
1015
1016
1017
1018 sub make_precat_copy {
1019         my $self = shift;
1020         my $copy = $self->copy;
1021
1022    if($copy) {
1023       $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1024
1025       $copy->editor($self->editor->requestor->id);
1026       $copy->edit_date('now');
1027       $copy->dummy_title($self->dummy_title);
1028       $copy->dummy_author($self->dummy_author);
1029
1030                 $self->update_copy();
1031                 return;
1032    }
1033
1034    $logger->info("circulator: Creating a new precataloged ".
1035                 "copy in checkout with barcode " . $self->copy_barcode);
1036
1037    $copy = Fieldmapper::asset::copy->new;
1038    $copy->circ_lib($self->circ_lib);
1039    $copy->creator($self->editor->requestor->id);
1040    $copy->editor($self->editor->requestor->id);
1041    $copy->barcode($self->copy_barcode);
1042    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1043    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1044    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1045
1046    $copy->dummy_title($self->dummy_title || "");
1047    $copy->dummy_author($self->dummy_author || "");
1048
1049         unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1050                 $self->bail_out(1);
1051                 $self->push_events($self->editor->event);
1052                 return;
1053         }       
1054
1055         # this is a little bit of a hack, but we need to 
1056         # get the copy into the script runner
1057         $self->script_runner->insert("environment.copy", $copy, 1);
1058 }
1059
1060
1061 sub checkout_noncat {
1062         my $self = shift;
1063
1064         my $circ;
1065         my $evt;
1066
1067    my $lib              = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1068    my $count    = $self->noncat_count || 1;
1069    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1070
1071    $logger->info("circ creating $count noncat circs with checkout time $cotime");
1072
1073    for(1..$count) {
1074
1075       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1076          $self->editor->requestor->id, 
1077                         $self->patron->id, 
1078                         $lib, 
1079                         $self->noncat_type, 
1080                         $cotime,
1081                         $self->editor );
1082
1083                 if( $evt ) {
1084                         $self->push_events($evt);
1085                         $self->bail_out(1);
1086                         return; 
1087                 }
1088                 $self->circ($circ);
1089    }
1090 }
1091
1092
1093 sub do_checkin {
1094         my $self = shift;
1095         $self->log_me("do_checkin()");
1096
1097         return $self->bail_on_events(
1098                 OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1099                 unless $self->copy;
1100
1101         unless( $self->is_renewal ) {
1102                 return $self->bail_on_events($self->editor->event)
1103                         unless $self->editor->allowed('COPY_CHECKIN');
1104         }
1105
1106         $self->push_events($self->check_copy_alert());
1107         $self->push_events($self->check_checkin_copy_status());
1108
1109
1110         # the renew code will have already found our circulation object
1111         unless( $self->is_renewal and $self->circ ) {
1112
1113                 # first lets see if we have a good old fashioned open circulation
1114                 my $circ = $self->editor->search_action_circulation(
1115                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1116
1117                 if(!$circ) {
1118                         # if not, lets look for other circs we can check in
1119                         $circ = $self->editor->search_action_circulation(
1120                                 { 
1121                                         target_copy => $self->copy->id, 
1122                                         xact_finish => undef,
1123                                         stop_fines      => [ 
1124                                                 OILS_STOP_FINES_CLAIMSRETURNED, 
1125                                                 OILS_STOP_FINES_LOST, 
1126                                                 OILS_STOP_FINES_LONGOVERDUE, 
1127                                         ]
1128                                 } )->[0];
1129                 }
1130
1131                 $self->circ($circ);
1132         }
1133
1134
1135         # if the circ is marked as 'claims returned', add the event to the list
1136         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1137                 if ($self->circ and $self->circ->stop_fines 
1138                                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1139
1140         # handle the overridable events 
1141         $self->override_events unless $self->is_renewal;
1142         return if $self->bail_out;
1143         
1144         if( $self->copy ) {
1145                 $self->transit(
1146                         $self->editor->search_action_transit_copy(
1147                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1148         }
1149
1150         if( $self->circ ) {
1151                 $self->checkin_handle_circ;
1152                 return if $self->bail_out;
1153                 $self->checkin_changed(1);
1154
1155         } elsif( $self->transit ) {
1156                 my $hold_transit = $self->process_received_transit;
1157                 $self->checkin_changed(1);
1158
1159                 if( $self->bail_out ) { 
1160                         $self->checkin_flesh_events;
1161                         return;
1162                 }
1163                 
1164                 if( my $e = $self->check_checkin_copy_status() ) {
1165                         # If the original copy status is special, alert the caller
1166                         return $self->bail_on_events($e);       
1167                 }
1168
1169                 if( $hold_transit ) {
1170                         $self->checkin_flesh_events;
1171                         return;
1172                 } 
1173         }
1174
1175         if( $self->is_renewal ) {
1176                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1177                 return;
1178         }
1179
1180    # ------------------------------------------------------------------------------
1181    # Circulations and transits are now closed where necessary.  Now go on to see if
1182    # this copy can fulfill a hold or needs to be routed to a different location
1183    # ------------------------------------------------------------------------------
1184
1185         if( $self->attempt_checkin_hold_capture() ) {
1186                 return if $self->bail_out;
1187
1188    } else { # not needed for a hold
1189
1190                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1191                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1192
1193                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1194
1195       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1196
1197                         $self->checkin_handle_precat();
1198                         return if $self->bail_out;
1199
1200       } else {
1201
1202                         $self->checkin_build_copy_transit();
1203                         return if $self->bail_out;
1204                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1205       }
1206    }
1207
1208
1209         $self->reshelve_copy;
1210         return if $self->bail_out;
1211
1212         unless($self->checkin_changed) {
1213
1214                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1215                 my $stat = (ref $self->copy->status) ? $self->copy->status->id : $self->copy->status;
1216
1217         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1218          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1219                 $self->bail_out(1); # no need to commit anything
1220
1221         } else {
1222                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1223                         unless @{$self->events};
1224         }
1225
1226         $self->checkin_flesh_events;
1227         return;
1228 }
1229
1230 sub reshelve_copy {
1231    my $self    = shift;
1232    my $copy    = $self->copy;
1233    my $force   = $self->force;
1234
1235    my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1236
1237    if($force || (
1238       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1239       $stat != OILS_COPY_STATUS_AVAILABLE and
1240       $stat != OILS_COPY_STATUS_CATALOGING and
1241       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1242       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1243
1244         $copy->status( OILS_COPY_STATUS_RESHELVING );
1245                         $self->update_copy;
1246                         $self->checkin_changed(1);
1247         }
1248 }
1249
1250
1251 sub checkin_handle_precat {
1252         my $self        = shift;
1253    my $copy    = $self->copy;
1254
1255    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1256       $copy->status(OILS_COPY_STATUS_CATALOGING);
1257                 $self->update_copy();
1258                 $self->checkin_changed(1);
1259                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1260    }
1261 }
1262
1263
1264 sub checkin_build_copy_transit {
1265         my $self                        = shift;
1266         my $copy       = $self->copy;
1267    my $transit    = Fieldmapper::action::transit_copy->new;
1268
1269    $transit->source($self->editor->requestor->ws_ou);
1270    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1271    $transit->target_copy($copy->id);
1272    $transit->source_send_time('now');
1273    $transit->copy_status( (ref $copy->status) ? $copy->status->id : $copy->status );
1274
1275         return $self->bail_on_events($self->editor->event)
1276                 unless $self->editor->create_action_transit_copy($transit);
1277
1278    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1279         $self->update_copy;
1280         $self->checkin_changed(1);
1281 }
1282
1283
1284 sub attempt_checkin_hold_capture {
1285         my $self = shift;
1286         my $copy = $self->copy;
1287
1288         # See if this copy can fulfill any holds
1289         my ($hold) = $holdcode->find_nearest_permitted_hold(
1290                 OpenSRF::AppSession->create('open-ils.storage'), 
1291                 $copy, $self->editor->requestor );
1292
1293         if(!$hold) {
1294                 $logger->debug("circulator: no potential permitted".
1295                         "holds found for copy ".$copy->barcode);
1296                 return undef;
1297         }
1298
1299         $logger->info("circulator: found permitted hold ".
1300                 $hold->id . " for copy, capturing...");
1301
1302         $hold->current_copy($copy->id);
1303         $hold->capture_time('now');
1304
1305         # prevent some DB errors
1306         $hold->clear_fulfillment_time;
1307         $hold->clear_fulfillment_staff;
1308         $hold->clear_fulfillment_lib;
1309         $hold->clear_expire_time; 
1310
1311         $self->bail_on_events($self->editor->event)
1312                 unless $self->editor->update_action_hold_request($hold);
1313         $self->hold($hold);
1314         $self->checkin_changed(1);
1315
1316         return 1 if $self->bail_out;
1317
1318         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1319
1320                 # This hold was captured in the correct location
1321         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1322                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1323         
1324         } else {
1325         
1326                 # Hold needs to be picked up elsewhere.  Build a hold
1327                 # transit and route the item.
1328                 $self->checkin_build_hold_transit();
1329         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1330                 return 1 if $self->bail_out;
1331                 $self->push_events(
1332                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1333         }
1334
1335         # make sure we save the copy status
1336         $self->update_copy;
1337         return 1;
1338 }
1339
1340
1341 sub checkin_build_hold_transit {
1342         my $self = shift;
1343
1344    my $copy = $self->copy;
1345    my $hold = $self->hold;
1346    my $trans = Fieldmapper::action::hold_transit_copy->new;
1347
1348         my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
1349    $trans->hold($hold->id);
1350    $trans->source($self->editor->requestor->ws_ou);
1351    $trans->dest($hold->pickup_lib);
1352    $trans->source_send_time("now");
1353    $trans->target_copy($copy->id);
1354
1355         # when the copy gets to its destination, it will recover
1356         # this status - put it onto the holds shelf
1357    $trans->copy_status(OILS_COPY_STATUS_IN_TRANSIT);
1358
1359         return $self->bail_on_events($self->editor->event)
1360                 unless $self->editor->create_action_hold_transit_copy($trans);
1361 }
1362
1363
1364
1365 sub process_received_transit {
1366         my $self = shift;
1367         my $copy = $self->copy;
1368    my $copyid = $self->copy->id;
1369
1370         my $status_name = $U->copy_status($copy->status)->name;
1371    $logger->debug("circulator: attempting transit receive on ".
1372                 "copy $copyid. Copy status is $status_name");
1373
1374         my $transit = $self->transit;
1375
1376    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1377       $logger->activity("Fowarding transit on copy which is destined ".
1378          "for a different location. copy=$copyid,current ".
1379          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1380
1381                 $self->bail_on_events(
1382                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1383    }
1384
1385    # The transit is received, set the receive time
1386    $transit->dest_recv_time('now');
1387         $self->bail_on_events($self->editor->event)
1388                 unless $self->editor->update_action_transit_copy($transit);
1389
1390         my $hold_transit = $self->editor->search_action_hold_transit_copy(
1391                 { hold => $transit->id }
1392         );
1393
1394    $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1395    $copy->status( $transit->copy_status );
1396         $self->update_copy();
1397         return if $self->bail_out;
1398
1399         my $ishold = ($hold_transit) ? 1 : 0;
1400
1401         $self->push_events( 
1402                 OpenILS::Event->new(
1403                 'SUCCESS', 
1404                 ishold => $ishold,
1405       payload => { transit => $transit, holdtransit => $hold_transit } ));
1406
1407         return $hold_transit;
1408 }
1409
1410
1411 sub checkin_handle_circ {
1412    my $self = shift;
1413         $U->logmark;
1414
1415    my $circ = $self->circ;
1416    my $copy = $self->copy;
1417    my $evt;
1418    my $obt;
1419
1420    # backdate the circ if necessary
1421    if($self->backdate) {
1422                 $self->checkin_handle_backdate;
1423                 return if $self->bail_out;
1424    }
1425
1426    if(!$circ->stop_fines) {
1427       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1428       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1429       $circ->stop_fines_time('now');
1430    }
1431
1432    # see if there are any fines owed on this circ.  if not, close it
1433         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1434    $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1435
1436    # Set the checkin vars since we have the item
1437    $circ->checkin_time('now');
1438    $circ->checkin_staff($self->editor->requestor->id);
1439    $circ->checkin_lib($self->editor->requestor->ws_ou);
1440
1441         $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1442         $self->update_copy;
1443
1444         return $self->bail_on_events($self->editor->event)
1445                 unless $self->editor->update_action_circulation($circ);
1446 }
1447
1448
1449 sub checkin_handle_backdate {
1450         my $self = shift;
1451
1452         my $bills = $self->editor->search_money_billing(
1453                 { billing_ts => { ">=" => $self->backdate }, "xact" => $self->circ->id }
1454         );
1455
1456         for my $bill (@$bills) {        
1457                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1458                         $bill->voided('t');
1459                         my $n = $bill->note || "";
1460                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1461
1462                         $self->bail_on_events($self->editor->event)
1463                                 unless $self->editor->update_money_billing($bill);
1464                 }
1465         }
1466 }
1467
1468
1469
1470 # XXX Legacy version for Circ.pm support
1471 sub _checkin_handle_backdate {
1472    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1473
1474    my $bills = $session->request(
1475       "open-ils.storage.direct.money.billing.search_where.atomic",
1476       billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1477
1478    if($bills) {
1479       for my $bill (@$bills) {
1480          $bill->voided('t');
1481          my $n = $bill->note || "";
1482          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1483          my $s = $session->request(
1484             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1485          return $U->DB_UPDATE_FAILED($bill) unless $s;
1486       }
1487    }
1488 }
1489
1490
1491
1492
1493
1494
1495 sub find_patron_from_copy {
1496         my $self = shift;
1497         my $circs = $self->editor->search_action_circulation(
1498                 { target_copy => $self->copy->id, stop_fines_time => undef });
1499         my $circ = $circs->[0];
1500         return unless $circ;
1501         my $u = $self->editor->retrieve_actor_user($circ->usr)
1502                 or return $self->bail_on_events($self->editor->event);
1503         $self->patron($u);
1504 }
1505
1506 sub check_checkin_copy_status {
1507         my $self = shift;
1508    my $copy = $self->copy;
1509
1510    my $islost     = 0;
1511    my $ismissing  = 0;
1512    my $evt        = undef;
1513
1514    my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1515
1516    return undef
1517       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1518             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1519             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1520             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1521             $status == OILS_COPY_STATUS_RESHELVING );
1522
1523    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1524       if( $status == OILS_COPY_STATUS_LOST );
1525
1526    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1527       if( $status == OILS_COPY_STATUS_MISSING );
1528
1529    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1530 }
1531
1532
1533
1534 # --------------------------------------------------------------------------
1535 # On checkin, we need to return as many relevant objects as we can
1536 # --------------------------------------------------------------------------
1537 sub checkin_flesh_events {
1538         my $self = shift;
1539
1540         for my $evt (@{$self->events}) {
1541
1542                 my $payload          = {};
1543                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1544                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1545                 $payload->{circ}     = $self->circ;
1546                 $payload->{transit}  = $self->transit;
1547                 $payload->{hold}     = $self->hold;
1548                 
1549                 $evt->{payload} = $payload;
1550         }
1551 }
1552
1553 sub log_me {
1554         my( $self, $msg ) = @_;
1555         my $bc = ($self->copy) ? $self->copy->barcode :
1556                 $self->barcode;
1557         $bc ||= "";
1558         my $usr = ($self->patron) ? $self->patron->id : "";
1559         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1560                 ", recipient=$usr, copy=$bc");
1561 }
1562
1563
1564 sub do_renew {
1565         my $self = shift;
1566         $self->log_me("do_renew()");
1567         $self->is_renewal(1);
1568
1569         unless( $self->is_renewal ) {
1570                 return $self->bail_on_events($self->editor->events)
1571                         unless $self->editor->allowed('RENEW_CIRC');
1572         }       
1573
1574         # Make sure there is an open circ to renew that is not
1575         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1576         my $circ = $self->editor->search_action_circulation(
1577                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1578
1579         return $self->bail_on_events($self->editor->event) unless $circ;
1580
1581         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1582                 if $circ->renewal_remaining < 1;
1583
1584         # -----------------------------------------------------------------
1585
1586         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1587         $self->renewal_remaining(0) if $self->renewal_remaining < 0;
1588         $self->circ($circ);
1589
1590         $self->run_renew_permit;
1591
1592         # Check the item in
1593         $self->do_checkin();
1594         return if $self->bail_out;
1595
1596         unless( $self->permit_override ) {
1597                 $self->do_permit();
1598                 return if $self->bail_out;
1599                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1600                 $self->remove_event('ITEM_NOT_CATALOGED');
1601         }       
1602
1603         $self->override_events;
1604         return if $self->bail_out;
1605
1606         $self->events([]);
1607         $self->do_checkout();
1608 }
1609
1610
1611 sub remove_event {
1612         my( $self, $evt ) = @_;
1613         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1614         $logger->debug("circulator: removing event from list: $evt");
1615         my @events = @{$self->events};
1616         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1617 }
1618
1619
1620 sub have_event {
1621         my( $self, $evt ) = @_;
1622         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1623         return grep { $_->{textcode} eq $evt } @{$self->events};
1624 }
1625
1626
1627
1628 sub run_renew_permit {
1629         my $self = shift;
1630    my $runner = $self->script_runner;
1631
1632    $runner->load($self->circ_permit_renew);
1633    my $result = $runner->run or 
1634                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1635    my $events = $result->{events};
1636
1637    $logger->activity("circ_permit_renew for user ".
1638       $self->patron->id." returned events: @$events") if @$events;
1639
1640         $self->push_events(OpenILS::Event->new($_)) for @$events;
1641 }
1642
1643
1644
1645