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