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