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