]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
added some logging
[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         unless( $self->is_renewal ) {
1075                 return $self->bail_on_events($self->editor->event)
1076                         unless $self->editor->allowed('COPY_CHECKIN');
1077         }
1078
1079         $self->push_events($self->check_copy_alert());
1080         $self->push_events($self->check_checkin_copy_status());
1081
1082
1083         # the renew code will have already found our circulation object
1084         unless( $self->is_renewal and $self->circ ) {
1085
1086                 # first lets see if we have a good old fashioned open circulation
1087                 my $circ = $self->editor->search_action_circulation(
1088                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1089
1090                 if(!$circ) {
1091                         # if not, lets look for other circs we can check in
1092                         $circ = $self->editor->search_action_circulation(
1093                                 { 
1094                                         target_copy => $self->copy->id, 
1095                                         xact_finish => undef,
1096                                         stop_fines      => [ 'CLAIMSRETURNED', 'LOST', 'LONGOVERDUE' ]
1097                                 } )->[0];
1098                 }
1099
1100                 $self->circ($circ);
1101         }
1102
1103
1104         # if the circ is marked as 'claims returned', add the event to the list
1105         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1106                 if ($self->circ and $self->circ->stop_fines 
1107                                 and $self->circ->stop_fines eq 'CLAIMSRETURNED');
1108
1109         # handle the overridable events 
1110         $self->override_events unless $self->is_renewal;
1111         
1112         if( $self->copy ) {
1113                 $self->transit(
1114                         $self->editor->search_action_transit_copy(
1115                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1116         }
1117
1118         if( $self->circ ) {
1119                 $self->checkin_handle_circ;
1120                 return if $self->bail_out;
1121                 $self->checkin_changed(1);
1122
1123         } elsif( $self->transit ) {
1124                 my $hold_transit = $self->process_received_transit;
1125                 $self->checkin_changed(1);
1126
1127                 if( $self->bail_out ) { 
1128                         $self->checkin_flesh_events;
1129                         return;
1130                 }
1131                 
1132                 if( my $e = $self->check_checkin_copy_status() ) {
1133                         # If the original copy status is special, alert the caller
1134                         return $self->bail_on_events($e);       
1135                 }
1136
1137                 if( $hold_transit ) {
1138                         $self->checkin_flesh_events;
1139                         return;
1140                 } 
1141         }
1142
1143         if( $self->is_renewal ) {
1144                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1145                 return;
1146         }
1147
1148    # ------------------------------------------------------------------------------
1149    # Circulations and transits are now closed where necessary.  Now go on to see if
1150    # this copy can fulfill a hold or needs to be routed to a different location
1151    # ------------------------------------------------------------------------------
1152
1153         if( $self->attempt_checkin_hold_capture() ) {
1154                 return if $self->bail_out;
1155
1156    } else { # not needed for a hold
1157
1158                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1159                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1160
1161                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1162
1163       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1164
1165                         $self->checkin_handle_precat();
1166                         return if $self->bail_out;
1167
1168       } else {
1169
1170                         $self->checkin_build_copy_transit();
1171                         return if $self->bail_out;
1172                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1173       }
1174    }
1175
1176
1177         $self->reshelve_copy;
1178         return if $self->bail_out;
1179
1180         unless($self->checkin_changed) {
1181
1182                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1183                 my $stat = (ref $self->copy->status) ? $self->copy->status->id : $self->copy->status;
1184
1185         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1186          if( $stat == $U->copy_status_from_name('on holds shelf')->id );
1187                 $self->bail_out(1); # no need to commit anything
1188
1189         } else {
1190                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1191                         unless @{$self->events};
1192         }
1193
1194         $self->checkin_flesh_events;
1195         return;
1196 }
1197
1198 sub reshelve_copy {
1199    my $self    = shift;
1200    my $copy    = $self->copy;
1201    my $force   = $self->force;
1202
1203    my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1204
1205    if($force || (
1206       $stat != $U->copy_status_from_name('on holds shelf')->id and
1207       $stat != $U->copy_status_from_name('available')->id and
1208       $stat != $U->copy_status_from_name('cataloging')->id and
1209       $stat != $U->copy_status_from_name('in transit')->id and
1210       $stat != $U->copy_status_from_name('reshelving')->id) ) {
1211
1212         $copy->status( $U->copy_status_from_name('reshelving') );
1213                         $self->update_copy;
1214                         $self->checkin_changed(1);
1215         }
1216 }
1217
1218
1219 sub checkin_handle_precat {
1220         my $self        = shift;
1221    my $copy    = $self->copy;
1222    my $catstat = $U->copy_status_from_name('cataloging');
1223
1224    if( $self->is_precat and ($copy->status != $catstat->id) ) {
1225       $copy->status($catstat);
1226                 $self->update_copy();
1227                 $self->checkin_changed(1);
1228                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1229    }
1230 }
1231
1232
1233 sub checkin_build_copy_transit {
1234         my $self                        = shift;
1235    my $copy       = $self->copy;
1236    my $transit    = Fieldmapper::action::transit_copy->new;
1237
1238    $transit->source($self->editor->requestor->ws_ou);
1239    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1240    $transit->target_copy($copy->id);
1241    $transit->source_send_time('now');
1242    $transit->copy_status( (ref $copy->status) ? $copy->status->id : $copy->status );
1243
1244         return $self->bail_on_events($self->editor->event)
1245                 unless $self->editor->create_action_transit_copy($transit);
1246
1247    $copy->status($U->copy_status_from_name('in transit'));
1248         $self->update_copy;
1249         $self->checkin_changed(1);
1250 }
1251
1252
1253 sub attempt_checkin_hold_capture {
1254         my $self = shift;
1255         my $copy = $self->copy;
1256
1257         # See if this copy can fulfill any holds
1258         my ($hold) = $holdcode->find_nearest_permitted_hold(
1259                 OpenSRF::AppSession->create('open-ils.storage'), 
1260                 $copy, $self->editor->requestor );
1261
1262         if(!$hold) {
1263                 $logger->debug("circulator: no potential permitted".
1264                         "holds found for copy ".$copy->barcode);
1265                 return undef;
1266         }
1267
1268         $logger->info("circulator: found permitted hold ".
1269                 $hold->id . " for copy, capturing...");
1270
1271         $hold->current_copy($copy->id);
1272         $hold->capture_time('now');
1273
1274         # prevent some DB errors
1275         $hold->clear_fulfillment_time;
1276         $hold->clear_fulfillment_staff;
1277         $hold->clear_fulfillment_lib;
1278         $hold->clear_expire_time; 
1279
1280         $self->bail_on_events($self->editor->event)
1281                 unless $self->editor->update_action_hold_request($hold);
1282         $self->hold($hold);
1283         $self->checkin_changed(1);
1284
1285         return 1 if $self->bail_out;
1286
1287         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1288
1289                 # This hold was captured in the correct location
1290         $copy->status( $U->copy_status_from_name('on holds shelf') );
1291                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1292         
1293         } else {
1294         
1295                 # Hold needs to be picked up elsewhere.  Build a hold
1296                 # transit and route the item.
1297                 $self->checkin_build_hold_transit();
1298         $copy->status($U->copy_status_from_name('in transit') );
1299                 return 1 if $self->bail_out;
1300                 $self->push_events(
1301                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1302         }
1303
1304         # make sure we save the copy status
1305         $self->update_copy;
1306         return 1;
1307 }
1308
1309
1310 sub checkin_build_hold_transit {
1311         my $self = shift;
1312
1313    my $copy = $self->copy;
1314    my $hold = $self->hold;
1315    my $trans = Fieldmapper::action::hold_transit_copy->new;
1316
1317         my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
1318    $trans->hold($hold->id);
1319    $trans->source($self->editor->requestor->ws_ou);
1320    $trans->dest($hold->pickup_lib);
1321    $trans->source_send_time("now");
1322    $trans->target_copy($copy->id);
1323    $trans->copy_status($stat);
1324
1325         return $self->bail_on_events($self->editor->event)
1326                 unless $self->editor->create_action_hold_transit_copy($trans);
1327 }
1328
1329
1330
1331 sub process_received_transit {
1332         my $self = shift;
1333         my $copy = $self->copy;
1334    my $copyid = $self->copy->id;
1335
1336    my $status_name = $U->copy_status_to_name($copy->status);
1337    $logger->debug("circulator: attempting transit receive on ".
1338                 "copy $copyid. Copy status is $status_name");
1339
1340         my $transit = $self->transit;
1341
1342    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1343       $logger->activity("Fowarding transit on copy which is destined ".
1344          "for a different location. copy=$copyid,current ".
1345          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1346
1347                 $self->bail_on_events(
1348                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1349    }
1350
1351    # The transit is received, set the receive time
1352    $transit->dest_recv_time('now');
1353         $self->bail_on_events($self->editor->event)
1354                 unless $self->editor->update_action_transit_copy($transit);
1355
1356         my $hold_transit = $self->editor->search_action_hold_transit_copy(
1357                 { hold => $transit->id }
1358         );
1359
1360    $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1361    $copy->status( $transit->copy_status );
1362         $self->update_copy();
1363         return if $self->bail_out;
1364
1365         my $ishold = ($hold_transit) ? 1 : 0;
1366
1367         $self->push_events( 
1368                 OpenILS::Event->new(
1369                 'SUCCESS', 
1370                 ishold => $ishold,
1371       payload => { transit => $transit, holdtransit => $hold_transit } ));
1372
1373         return $hold_transit;
1374 }
1375
1376
1377 sub checkin_handle_circ {
1378    my $self = shift;
1379         $U->logmark;
1380
1381    my $circ = $self->circ;
1382    my $copy = $self->copy;
1383    my $evt;
1384    my $obt;
1385
1386    # backdate the circ if necessary
1387    if($self->backdate) {
1388                 $self->handle_backdate;
1389                 return if $self->bail_out;
1390    }
1391
1392    if(!$circ->stop_fines) {
1393       $circ->stop_fines('CHECKIN');
1394       $circ->stop_fines('RENEW') if $self->is_renewal;
1395       $circ->stop_fines_time('now');
1396    }
1397
1398    # see if there are any fines owed on this circ.  if not, close it
1399         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1400    $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1401
1402    # Set the checkin vars since we have the item
1403    $circ->checkin_time('now');
1404    $circ->checkin_staff($self->editor->requestor->id);
1405    $circ->checkin_lib($self->editor->requestor->ws_ou);
1406
1407         $self->copy->status($U->copy_status_from_name('reshelving'));
1408         $self->update_copy;
1409
1410         return $self->bail_on_events($self->editor->event)
1411                 unless $self->editor->update_action_circulation($circ);
1412 }
1413
1414
1415 sub checkin_handle_backdate {
1416         my $self = shift;
1417
1418         my $bills = $self->editor->search_money_billing(
1419                 { billing_ts => { ">=" => $self->backdate }, "xact" => $self->circ->id }
1420         );
1421
1422         for my $bill (@$bills) {        
1423                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1424                         $bill->voided('t');
1425                         my $n = $bill->note || "";
1426                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1427
1428                         $self->bail_on_events($self->editor->event)
1429                                 unless $self->editor->update_money_billing($bill);
1430                 }
1431         }
1432 }
1433
1434
1435
1436 # XXX Legacy version for Circ.pm support
1437 sub _checkin_handle_backdate {
1438    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1439
1440    my $bills = $session->request(
1441       "open-ils.storage.direct.money.billing.search_where.atomic",
1442       billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1443
1444    if($bills) {
1445       for my $bill (@$bills) {
1446          $bill->voided('t');
1447          my $n = $bill->note || "";
1448          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1449          my $s = $session->request(
1450             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1451          return $U->DB_UPDATE_FAILED($bill) unless $s;
1452       }
1453    }
1454 }
1455
1456
1457
1458
1459
1460
1461 sub find_patron_from_copy {
1462         my $self = shift;
1463         my $circs = $self->editor->search_action_circulation(
1464                 { target_copy => $self->copy->id, stop_fines_time => undef });
1465         my $circ = $circs->[0];
1466         return unless $circ;
1467         my $u = $self->editor->retrieve_actor_user($circ->usr)
1468                 or return $self->bail_on_events($self->editor->event);
1469         $self->patron($u);
1470 }
1471
1472 sub check_checkin_copy_status {
1473         my $self = shift;
1474    my $copy = $self->copy;
1475
1476    my $islost     = 0;
1477    my $ismissing  = 0;
1478    my $evt        = undef;
1479
1480    my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1481
1482    return undef
1483       if(   $status == $U->copy_status_from_name('available')->id    ||
1484             $status == $U->copy_status_from_name('checked out')->id  ||
1485             $status == $U->copy_status_from_name('in process')->id   ||
1486             $status == $U->copy_status_from_name('in transit')->id   ||
1487             $status == $U->copy_status_from_name('reshelving')->id );
1488
1489    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1490       if( $status == $U->copy_status_from_name('lost')->id );
1491
1492    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1493       if( $status == $U->copy_status_from_name('missing')->id );
1494
1495    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1496 }
1497
1498
1499
1500 # --------------------------------------------------------------------------
1501 # On checkin, we need to return as many relevant objects as we can
1502 # --------------------------------------------------------------------------
1503 sub checkin_flesh_events {
1504         my $self = shift;
1505
1506         for my $evt (@{$self->events}) {
1507
1508                 my $payload          = {};
1509                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1510                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1511                 $payload->{circ}     = $self->circ;
1512                 $payload->{transit}  = $self->transit;
1513                 $payload->{hold}     = $self->hold;
1514                 
1515                 $evt->{payload} = $payload;
1516         }
1517 }
1518
1519 sub log_me {
1520         my( $self, $msg ) = @_;
1521         my $bc = ($self->copy) ? $self->copy->barcode :
1522                 $self->barcode;
1523         $bc ||= "";
1524         my $usr = ($self->patron) ? $self->patron->id : "";
1525         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1526                 ", recipient=$usr, copy=$bc");
1527 }
1528
1529
1530 sub do_renew {
1531         my $self = shift;
1532         $self->log_me("do_renew()");
1533         $self->is_renewal(1);
1534
1535         unless( $self->is_renewal ) {
1536                 return $self->bail_on_events($self->editor->events)
1537                         unless $self->editor->allowed('RENEW_CIRC');
1538         }       
1539
1540         # Make sure there is an open circ to renew that is not
1541         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1542         my $circ = $self->editor->search_action_circulation(
1543                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1544
1545         return $self->bail_on_events($self->editor->event) unless $circ;
1546
1547         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1548                 if $circ->renewal_remaining < 1;
1549
1550         # -----------------------------------------------------------------
1551
1552         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1553         $self->renewal_remaining(0) if $self->renewal_remaining < 0;
1554         $self->circ($circ);
1555
1556         $self->run_renew_permit;
1557
1558         # Check the item in
1559         $self->do_checkin();
1560         return if $self->bail_out;
1561
1562         unless( $self->permit_override ) {
1563                 $self->do_permit();
1564                 return if $self->bail_out;
1565                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1566                 $self->remove_event('ITEM_NOT_CATALOGED');
1567         }       
1568
1569         $self->override_events;
1570         return if $self->bail_out;
1571
1572         $self->events([]);
1573         $self->do_checkout();
1574 }
1575
1576
1577 sub remove_event {
1578         my( $self, $evt ) = @_;
1579         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1580         $logger->debug("circulator: removing event from list: $evt");
1581         my @events = @{$self->events};
1582         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1583 }
1584
1585
1586 sub have_event {
1587         my( $self, $evt ) = @_;
1588         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1589         return grep { $_->{textcode} eq $evt } @{$self->events};
1590 }
1591
1592
1593
1594 sub run_renew_permit {
1595         my $self = shift;
1596    my $runner = $self->script_runner;
1597
1598    $runner->load($self->circ_permit_renew);
1599    my $result = $runner->run or 
1600                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1601    my $events = $result->{events};
1602
1603    $logger->activity("circ_permit_renew for user ".
1604       $self->patron->id." returned events: @$events") if @$events;
1605
1606         $self->push_events(OpenILS::Event->new($_)) for @$events;
1607 }
1608
1609
1610
1611