]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
moved to cancel_time on hold as opposed to deleting it
[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} = $copy 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
813         my $holds       = $self->editor->search_action_hold_request(
814                 { 
815                         current_copy            =>  $copy->id , 
816                         cancel_time                     => undef, 
817                         fulfillment_time        => undef 
818                 }
819         );
820
821    my @fulfilled;
822
823    # XXX We should only fulfill one hold here...
824    # XXX If a hold was transited to the user who is checking out
825    # the item, we need to make sure that hold is what's grabbed
826    if(@$holds) {
827
828       # for now, just sort by id to get what should be the oldest hold
829       $holds = [ sort { $a->id <=> $b->id } @$holds ];
830       my @myholds = grep { $_->usr eq $patron->id } @$holds;
831       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
832
833       if(@myholds) {
834          my $hold = $myholds[0];
835
836          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
837
838          # if the hold was never officially captured, capture it.
839          $hold->capture_time('now') unless $hold->capture_time;
840
841                         # just make sure it's set correctly
842          $hold->current_copy($copy->id); 
843
844          $hold->fulfillment_time('now');
845                         $hold->fulfillment_staff($self->editor->requestor->id);
846                         $hold->fulfillment_lib($self->editor->requestor->ws_ou);
847
848                         return $self->bail_on_events($self->editor->event)
849                                 unless $self->editor->update_action_hold_request($hold);
850
851          push( @fulfilled, $hold->id );
852       }
853
854       # If there are any holds placed for other users that point to this copy,
855       # then we need to un-target those holds so the targeter can pick a new copy
856       for(@altholds) {
857
858          $logger->info("circulator: un-targeting hold ".$_->id.
859             " because copy ".$copy->id." is getting checked out");
860
861                         # - make the targeter process this hold at next run
862          $_->clear_prev_check_time; 
863
864                         # - clear out the targetted copy
865          $_->clear_current_copy;
866
867                         return $self->bail_on_event($self->editor->event)
868                                 unless $self->editor->update_action_hold_request($_);
869       }
870    }
871
872         $self->fulfilled_holds(\@fulfilled);
873 }
874
875
876
877 sub run_checkout_scripts {
878         my $self = shift;
879
880         my $evt;
881    my $runner = $self->script_runner;
882    $runner->load($self->circ_duration);
883
884    my $result = $runner->run or 
885                 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
886
887    my $duration   = $result->{durationRule};
888    my $dur_level  = $result->{durationLevel};
889    my $recurring  = $result->{recurringFinesRule};
890    my $max_fine   = $result->{maxFine};
891    my $rec_fines_level = $result->{recurringFinesLevel};
892
893    ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
894         return $self->bail_on_events($evt) if $evt;
895    ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
896         return $self->bail_on_events($evt) if $evt;
897    ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
898         return $self->bail_on_events($evt) if $evt;
899
900    $self->duration_level($dur_level);
901    $self->recurring_fines_level($rec_fines_level);
902    $self->duration_rule($duration);
903    $self->recurring_fines_rule($recurring);
904    $self->max_fine_rule($max_fine);
905 }
906
907
908 sub build_checkout_circ_object {
909         my $self = shift;
910
911    my $circ       = Fieldmapper::action::circulation->new;
912    my $duration   = $self->duration_rule;
913    my $max        = $self->max_fine_rule;
914    my $recurring  = $self->recurring_fines_rule;
915    my $copy       = $self->copy;
916    my $patron     = $self->patron;
917    my $dur_level  = $self->duration_level;
918    my $rec_level  = $self->recurring_fines_level;
919
920    $circ->duration( $duration->shrt ) if ($dur_level == 1);
921    $circ->duration( $duration->normal ) if ($dur_level == 2);
922    $circ->duration( $duration->extended ) if ($dur_level == 3);
923
924    $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
925    $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
926    $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
927
928    $circ->duration_rule( $duration->name );
929    $circ->recuring_fine_rule( $recurring->name );
930    $circ->max_fine_rule( $max->name );
931    $circ->max_fine( $max->amount );
932
933    $circ->fine_interval($recurring->recurance_interval);
934    $circ->renewal_remaining( $duration->max_renewals );
935    $circ->target_copy( $copy->id );
936    $circ->usr( $patron->id );
937    $circ->circ_lib( $self->circ_lib );
938
939    if( $self->is_renewal ) {
940       $circ->opac_renewal(1);
941       $circ->renewal_remaining($self->renewal_remaining);
942       $circ->circ_staff($self->editor->requestor->id);
943    }
944
945
946    # if the user provided an overiding checkout time,
947    # (e.g. the checkout really happened several hours ago), then
948    # we apply that here.  Does this need a perm??
949         $circ->xact_start(clense_ISO8601($self->checkout_time))
950                 if $self->checkout_time;
951
952    # if a patron is renewing, 'requestor' will be the patron
953    $circ->circ_staff($self->editor->requestor->id);
954         $circ->due_date( $self->create_due_date($circ->duration) );
955
956         $self->circ($circ);
957 }
958
959
960 sub apply_modified_due_date {
961         my $self = shift;
962         my $circ = $self->circ;
963         my $copy = $self->copy;
964
965    if( $self->due_date ) {
966
967                 return $self->bail_on_events($self->editor->event)
968                         unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
969
970       $circ->due_date(clense_ISO8601($self->due_date));
971
972    } else {
973
974       # if the due_date lands on a day when the location is closed
975       return unless $copy;
976
977                 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
978
979       $logger->info("circ searching for closed date overlap on lib $org".
980                         " with an item due date of ".$circ->due_date );
981
982       my $dateinfo = $U->storagereq(
983          'open-ils.storage.actor.org_unit.closed_date.overlap', 
984                         $org, $circ->due_date );
985
986       if($dateinfo) {
987          $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
988             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
989
990             # XXX make the behavior more dynamic
991             # for now, we just push the due date to after the close date
992             $circ->due_date($dateinfo->{end});
993       }
994    }
995 }
996
997
998
999 sub create_due_date {
1000         my( $self, $duration ) = @_;
1001    my ($sec,$min,$hour,$mday,$mon,$year) =
1002       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1003    $year += 1900; $mon += 1;
1004    my $due_date = sprintf(
1005       '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1006       $year, $mon, $mday, $hour, $min, $sec);
1007    return $due_date;
1008 }
1009
1010
1011
1012 sub make_precat_copy {
1013         my $self = shift;
1014         my $copy = $self->copy;
1015
1016    if($copy) {
1017       $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
1018
1019       $copy->editor($self->editor->requestor->id);
1020       $copy->edit_date('now');
1021       $copy->dummy_title($self->dummy_title);
1022       $copy->dummy_author($self->dummy_author);
1023
1024                 $self->update_copy();
1025                 return;
1026    }
1027
1028    $logger->info("circulator: Creating a new precataloged ".
1029                 "copy in checkout with barcode " . $self->copy_barcode);
1030
1031    $copy = Fieldmapper::asset::copy->new;
1032    $copy->circ_lib($self->circ_lib);
1033    $copy->creator($self->editor->requestor->id);
1034    $copy->editor($self->editor->requestor->id);
1035    $copy->barcode($self->copy_barcode);
1036    $copy->call_number(-1); #special CN for precat materials
1037    $copy->loan_duration(&PRECAT_LOAN_DURATION);
1038    $copy->fine_level(&PRECAT_FINE_LEVEL);
1039
1040    $copy->dummy_title($self->dummy_title || "");
1041    $copy->dummy_author($self->dummy_author || "");
1042
1043         unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1044                 $self->bail_out(1);
1045                 $self->push_events($self->editor->event);
1046                 return;
1047         }       
1048
1049         # this is a little bit of a hack, but we need to 
1050         # get the copy into the script runner
1051         $self->script_runner->insert("environment.copy", $copy, 1);
1052 }
1053
1054
1055 sub checkout_noncat {
1056         my $self = shift;
1057
1058         my $circ;
1059         my $evt;
1060
1061    my $lib              = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1062    my $count    = $self->noncat_count || 1;
1063    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1064
1065    $logger->info("circ creating $count noncat circs with checkout time $cotime");
1066
1067    for(1..$count) {
1068
1069       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1070          $self->editor->requestor->id, 
1071                         $self->patron->id, 
1072                         $lib, 
1073                         $self->noncat_type, 
1074                         $cotime,
1075                         $self->editor );
1076
1077                 if( $evt ) {
1078                         $self->push_events($evt);
1079                         $self->bail_out(1);
1080                         return; 
1081                 }
1082                 $self->circ($circ);
1083    }
1084 }
1085
1086
1087 sub do_checkin {
1088         my $self = shift;
1089         $self->log_me("do_checkin()");
1090
1091         return $self->bail_on_events(
1092                 OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1093                 unless $self->copy;
1094
1095         unless( $self->is_renewal ) {
1096                 return $self->bail_on_events($self->editor->event)
1097                         unless $self->editor->allowed('COPY_CHECKIN');
1098         }
1099
1100         $self->push_events($self->check_copy_alert());
1101         $self->push_events($self->check_checkin_copy_status());
1102
1103
1104         # the renew code will have already found our circulation object
1105         unless( $self->is_renewal and $self->circ ) {
1106
1107                 # first lets see if we have a good old fashioned open circulation
1108                 my $circ = $self->editor->search_action_circulation(
1109                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1110
1111                 if(!$circ) {
1112                         # if not, lets look for other circs we can check in
1113                         $circ = $self->editor->search_action_circulation(
1114                                 { 
1115                                         target_copy => $self->copy->id, 
1116                                         xact_finish => undef,
1117                                         stop_fines      => [ 'CLAIMSRETURNED', 'LOST', 'LONGOVERDUE' ]
1118                                 } )->[0];
1119                 }
1120
1121                 $self->circ($circ);
1122         }
1123
1124
1125         # if the circ is marked as 'claims returned', add the event to the list
1126         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1127                 if ($self->circ and $self->circ->stop_fines 
1128                                 and $self->circ->stop_fines eq 'CLAIMSRETURNED');
1129
1130         # handle the overridable events 
1131         $self->override_events unless $self->is_renewal;
1132         return if $self->bail_out;
1133         
1134         if( $self->copy ) {
1135                 $self->transit(
1136                         $self->editor->search_action_transit_copy(
1137                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1138         }
1139
1140         if( $self->circ ) {
1141                 $self->checkin_handle_circ;
1142                 return if $self->bail_out;
1143                 $self->checkin_changed(1);
1144
1145         } elsif( $self->transit ) {
1146                 my $hold_transit = $self->process_received_transit;
1147                 $self->checkin_changed(1);
1148
1149                 if( $self->bail_out ) { 
1150                         $self->checkin_flesh_events;
1151                         return;
1152                 }
1153                 
1154                 if( my $e = $self->check_checkin_copy_status() ) {
1155                         # If the original copy status is special, alert the caller
1156                         return $self->bail_on_events($e);       
1157                 }
1158
1159                 if( $hold_transit ) {
1160                         $self->checkin_flesh_events;
1161                         return;
1162                 } 
1163         }
1164
1165         if( $self->is_renewal ) {
1166                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1167                 return;
1168         }
1169
1170    # ------------------------------------------------------------------------------
1171    # Circulations and transits are now closed where necessary.  Now go on to see if
1172    # this copy can fulfill a hold or needs to be routed to a different location
1173    # ------------------------------------------------------------------------------
1174
1175         if( $self->attempt_checkin_hold_capture() ) {
1176                 return if $self->bail_out;
1177
1178    } else { # not needed for a hold
1179
1180                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1181                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1182
1183                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1184
1185       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1186
1187                         $self->checkin_handle_precat();
1188                         return if $self->bail_out;
1189
1190       } else {
1191
1192                         $self->checkin_build_copy_transit();
1193                         return if $self->bail_out;
1194                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1195       }
1196    }
1197
1198
1199         $self->reshelve_copy;
1200         return if $self->bail_out;
1201
1202         unless($self->checkin_changed) {
1203
1204                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1205                 my $stat = (ref $self->copy->status) ? $self->copy->status->id : $self->copy->status;
1206
1207         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1208          if( $stat == $U->copy_status_from_name('on holds shelf')->id );
1209                 $self->bail_out(1); # no need to commit anything
1210
1211         } else {
1212                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1213                         unless @{$self->events};
1214         }
1215
1216         $self->checkin_flesh_events;
1217         return;
1218 }
1219
1220 sub reshelve_copy {
1221    my $self    = shift;
1222    my $copy    = $self->copy;
1223    my $force   = $self->force;
1224
1225    my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1226
1227    if($force || (
1228       $stat != $U->copy_status_from_name('on holds shelf')->id and
1229       $stat != $U->copy_status_from_name('available')->id and
1230       $stat != $U->copy_status_from_name('cataloging')->id and
1231       $stat != $U->copy_status_from_name('in transit')->id and
1232       $stat != $U->copy_status_from_name('reshelving')->id) ) {
1233
1234         $copy->status( $U->copy_status_from_name('reshelving') );
1235                         $self->update_copy;
1236                         $self->checkin_changed(1);
1237         }
1238 }
1239
1240
1241 sub checkin_handle_precat {
1242         my $self        = shift;
1243    my $copy    = $self->copy;
1244    my $catstat = $U->copy_status_from_name('cataloging');
1245
1246    if( $self->is_precat and ($copy->status != $catstat->id) ) {
1247       $copy->status($catstat);
1248                 $self->update_copy();
1249                 $self->checkin_changed(1);
1250                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1251    }
1252 }
1253
1254
1255 sub checkin_build_copy_transit {
1256         my $self                        = shift;
1257    my $copy       = $self->copy;
1258    my $transit    = Fieldmapper::action::transit_copy->new;
1259
1260    $transit->source($self->editor->requestor->ws_ou);
1261    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1262    $transit->target_copy($copy->id);
1263    $transit->source_send_time('now');
1264    $transit->copy_status( (ref $copy->status) ? $copy->status->id : $copy->status );
1265
1266         return $self->bail_on_events($self->editor->event)
1267                 unless $self->editor->create_action_transit_copy($transit);
1268
1269    $copy->status($U->copy_status_from_name('in transit'));
1270         $self->update_copy;
1271         $self->checkin_changed(1);
1272 }
1273
1274
1275 sub attempt_checkin_hold_capture {
1276         my $self = shift;
1277         my $copy = $self->copy;
1278
1279         # See if this copy can fulfill any holds
1280         my ($hold) = $holdcode->find_nearest_permitted_hold(
1281                 OpenSRF::AppSession->create('open-ils.storage'), 
1282                 $copy, $self->editor->requestor );
1283
1284         if(!$hold) {
1285                 $logger->debug("circulator: no potential permitted".
1286                         "holds found for copy ".$copy->barcode);
1287                 return undef;
1288         }
1289
1290         $logger->info("circulator: found permitted hold ".
1291                 $hold->id . " for copy, capturing...");
1292
1293         $hold->current_copy($copy->id);
1294         $hold->capture_time('now');
1295
1296         # prevent some DB errors
1297         $hold->clear_fulfillment_time;
1298         $hold->clear_fulfillment_staff;
1299         $hold->clear_fulfillment_lib;
1300         $hold->clear_expire_time; 
1301
1302         $self->bail_on_events($self->editor->event)
1303                 unless $self->editor->update_action_hold_request($hold);
1304         $self->hold($hold);
1305         $self->checkin_changed(1);
1306
1307         return 1 if $self->bail_out;
1308
1309         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1310
1311                 # This hold was captured in the correct location
1312         $copy->status( $U->copy_status_from_name('on holds shelf') );
1313                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1314         
1315         } else {
1316         
1317                 # Hold needs to be picked up elsewhere.  Build a hold
1318                 # transit and route the item.
1319                 $self->checkin_build_hold_transit();
1320         $copy->status($U->copy_status_from_name('in transit') );
1321                 return 1 if $self->bail_out;
1322                 $self->push_events(
1323                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1324         }
1325
1326         # make sure we save the copy status
1327         $self->update_copy;
1328         return 1;
1329 }
1330
1331
1332 sub checkin_build_hold_transit {
1333         my $self = shift;
1334
1335    my $copy = $self->copy;
1336    my $hold = $self->hold;
1337    my $trans = Fieldmapper::action::hold_transit_copy->new;
1338
1339         my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
1340    $trans->hold($hold->id);
1341    $trans->source($self->editor->requestor->ws_ou);
1342    $trans->dest($hold->pickup_lib);
1343    $trans->source_send_time("now");
1344    $trans->target_copy($copy->id);
1345    $trans->copy_status($stat);
1346
1347         return $self->bail_on_events($self->editor->event)
1348                 unless $self->editor->create_action_hold_transit_copy($trans);
1349 }
1350
1351
1352
1353 sub process_received_transit {
1354         my $self = shift;
1355         my $copy = $self->copy;
1356    my $copyid = $self->copy->id;
1357
1358    my $status_name = $U->copy_status_to_name($copy->status);
1359    $logger->debug("circulator: attempting transit receive on ".
1360                 "copy $copyid. Copy status is $status_name");
1361
1362         my $transit = $self->transit;
1363
1364    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1365       $logger->activity("Fowarding transit on copy which is destined ".
1366          "for a different location. copy=$copyid,current ".
1367          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1368
1369                 $self->bail_on_events(
1370                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1371    }
1372
1373    # The transit is received, set the receive time
1374    $transit->dest_recv_time('now');
1375         $self->bail_on_events($self->editor->event)
1376                 unless $self->editor->update_action_transit_copy($transit);
1377
1378         my $hold_transit = $self->editor->search_action_hold_transit_copy(
1379                 { hold => $transit->id }
1380         );
1381
1382    $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1383    $copy->status( $transit->copy_status );
1384         $self->update_copy();
1385         return if $self->bail_out;
1386
1387         my $ishold = ($hold_transit) ? 1 : 0;
1388
1389         $self->push_events( 
1390                 OpenILS::Event->new(
1391                 'SUCCESS', 
1392                 ishold => $ishold,
1393       payload => { transit => $transit, holdtransit => $hold_transit } ));
1394
1395         return $hold_transit;
1396 }
1397
1398
1399 sub checkin_handle_circ {
1400    my $self = shift;
1401         $U->logmark;
1402
1403    my $circ = $self->circ;
1404    my $copy = $self->copy;
1405    my $evt;
1406    my $obt;
1407
1408    # backdate the circ if necessary
1409    if($self->backdate) {
1410                 $self->checkin_handle_backdate;
1411                 return if $self->bail_out;
1412    }
1413
1414    if(!$circ->stop_fines) {
1415       $circ->stop_fines('CHECKIN');
1416       $circ->stop_fines('RENEW') if $self->is_renewal;
1417       $circ->stop_fines_time('now');
1418    }
1419
1420    # see if there are any fines owed on this circ.  if not, close it
1421         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1422    $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1423
1424    # Set the checkin vars since we have the item
1425    $circ->checkin_time('now');
1426    $circ->checkin_staff($self->editor->requestor->id);
1427    $circ->checkin_lib($self->editor->requestor->ws_ou);
1428
1429         $self->copy->status($U->copy_status_from_name('reshelving'));
1430         $self->update_copy;
1431
1432         return $self->bail_on_events($self->editor->event)
1433                 unless $self->editor->update_action_circulation($circ);
1434 }
1435
1436
1437 sub checkin_handle_backdate {
1438         my $self = shift;
1439
1440         my $bills = $self->editor->search_money_billing(
1441                 { billing_ts => { ">=" => $self->backdate }, "xact" => $self->circ->id }
1442         );
1443
1444         for my $bill (@$bills) {        
1445                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1446                         $bill->voided('t');
1447                         my $n = $bill->note || "";
1448                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1449
1450                         $self->bail_on_events($self->editor->event)
1451                                 unless $self->editor->update_money_billing($bill);
1452                 }
1453         }
1454 }
1455
1456
1457
1458 # XXX Legacy version for Circ.pm support
1459 sub _checkin_handle_backdate {
1460    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1461
1462    my $bills = $session->request(
1463       "open-ils.storage.direct.money.billing.search_where.atomic",
1464       billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1465
1466    if($bills) {
1467       for my $bill (@$bills) {
1468          $bill->voided('t');
1469          my $n = $bill->note || "";
1470          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1471          my $s = $session->request(
1472             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1473          return $U->DB_UPDATE_FAILED($bill) unless $s;
1474       }
1475    }
1476 }
1477
1478
1479
1480
1481
1482
1483 sub find_patron_from_copy {
1484         my $self = shift;
1485         my $circs = $self->editor->search_action_circulation(
1486                 { target_copy => $self->copy->id, stop_fines_time => undef });
1487         my $circ = $circs->[0];
1488         return unless $circ;
1489         my $u = $self->editor->retrieve_actor_user($circ->usr)
1490                 or return $self->bail_on_events($self->editor->event);
1491         $self->patron($u);
1492 }
1493
1494 sub check_checkin_copy_status {
1495         my $self = shift;
1496    my $copy = $self->copy;
1497
1498    my $islost     = 0;
1499    my $ismissing  = 0;
1500    my $evt        = undef;
1501
1502    my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1503
1504    return undef
1505       if(   $status == $U->copy_status_from_name('available')->id    ||
1506             $status == $U->copy_status_from_name('checked out')->id  ||
1507             $status == $U->copy_status_from_name('in process')->id   ||
1508             $status == $U->copy_status_from_name('in transit')->id   ||
1509             $status == $U->copy_status_from_name('reshelving')->id );
1510
1511    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1512       if( $status == $U->copy_status_from_name('lost')->id );
1513
1514    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1515       if( $status == $U->copy_status_from_name('missing')->id );
1516
1517    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1518 }
1519
1520
1521
1522 # --------------------------------------------------------------------------
1523 # On checkin, we need to return as many relevant objects as we can
1524 # --------------------------------------------------------------------------
1525 sub checkin_flesh_events {
1526         my $self = shift;
1527
1528         for my $evt (@{$self->events}) {
1529
1530                 my $payload          = {};
1531                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1532                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1533                 $payload->{circ}     = $self->circ;
1534                 $payload->{transit}  = $self->transit;
1535                 $payload->{hold}     = $self->hold;
1536                 
1537                 $evt->{payload} = $payload;
1538         }
1539 }
1540
1541 sub log_me {
1542         my( $self, $msg ) = @_;
1543         my $bc = ($self->copy) ? $self->copy->barcode :
1544                 $self->barcode;
1545         $bc ||= "";
1546         my $usr = ($self->patron) ? $self->patron->id : "";
1547         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1548                 ", recipient=$usr, copy=$bc");
1549 }
1550
1551
1552 sub do_renew {
1553         my $self = shift;
1554         $self->log_me("do_renew()");
1555         $self->is_renewal(1);
1556
1557         unless( $self->is_renewal ) {
1558                 return $self->bail_on_events($self->editor->events)
1559                         unless $self->editor->allowed('RENEW_CIRC');
1560         }       
1561
1562         # Make sure there is an open circ to renew that is not
1563         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1564         my $circ = $self->editor->search_action_circulation(
1565                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1566
1567         return $self->bail_on_events($self->editor->event) unless $circ;
1568
1569         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1570                 if $circ->renewal_remaining < 1;
1571
1572         # -----------------------------------------------------------------
1573
1574         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1575         $self->renewal_remaining(0) if $self->renewal_remaining < 0;
1576         $self->circ($circ);
1577
1578         $self->run_renew_permit;
1579
1580         # Check the item in
1581         $self->do_checkin();
1582         return if $self->bail_out;
1583
1584         unless( $self->permit_override ) {
1585                 $self->do_permit();
1586                 return if $self->bail_out;
1587                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1588                 $self->remove_event('ITEM_NOT_CATALOGED');
1589         }       
1590
1591         $self->override_events;
1592         return if $self->bail_out;
1593
1594         $self->events([]);
1595         $self->do_checkout();
1596 }
1597
1598
1599 sub remove_event {
1600         my( $self, $evt ) = @_;
1601         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1602         $logger->debug("circulator: removing event from list: $evt");
1603         my @events = @{$self->events};
1604         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1605 }
1606
1607
1608 sub have_event {
1609         my( $self, $evt ) = @_;
1610         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1611         return grep { $_->{textcode} eq $evt } @{$self->events};
1612 }
1613
1614
1615
1616 sub run_renew_permit {
1617         my $self = shift;
1618    my $runner = $self->script_runner;
1619
1620    $runner->load($self->circ_permit_renew);
1621    my $result = $runner->run or 
1622                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1623    my $events = $result->{events};
1624
1625    $logger->activity("circ_permit_renew for user ".
1626       $self->patron->id." returned events: @$events") if @$events;
1627
1628         $self->push_events(OpenILS::Event->new($_)) for @$events;
1629 }
1630
1631
1632
1633