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