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