]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
moved scriptbuilder to new storage method for fetching total owed by a patron
[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::Const qw/:const/;
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                 "circulator: 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 __PACKAGE__->register_method(
133         method  => "run_method",
134         api_name        => "open-ils.circ.checkout.full");
135 __PACKAGE__->register_method(
136         method  => "run_method",
137         api_name        => "open-ils.circ.checkout.full.override");
138
139
140
141 sub run_method {
142         my( $self, $conn, $auth, $args ) = @_;
143         translate_legacy_args($args);
144         my $api = $self->api_name;
145
146         my $circulator = 
147                 OpenILS::Application::Circ::Circulator->new($auth, %$args);
148
149         return circ_events($circulator) if $circulator->bail_out;
150
151         # --------------------------------------------------------------------------
152         # Go ahead and load the script runner to make sure we have all 
153         # of the objects we need
154         # --------------------------------------------------------------------------
155         $circulator->is_renewal(1) if $api =~ /renew/;
156         $circulator->is_checkin(1) if $api =~ /checkin/;
157         $circulator->mk_script_runner;
158         return circ_events($circulator) if $circulator->bail_out;
159
160         $circulator->circ_permit_patron($scripts{circ_permit_patron});
161         $circulator->circ_permit_copy($scripts{circ_permit_copy});              
162         $circulator->circ_duration($scripts{circ_duration});                     
163         $circulator->circ_permit_renew($scripts{circ_permit_renew});
164         
165         $circulator->override(1) if $api =~ /override/o;
166
167         if( $api =~ /checkout\.permit/ ) {
168                 $circulator->do_permit();
169
170         } elsif( $api =~ /checkout.full/ ) {
171
172                 $circulator->do_permit();
173                 unless( $circulator->bail_out ) {
174                         $circulator->events([]);
175                         $circulator->do_checkout();
176                 }
177
178         } elsif( $api =~ /checkout/ ) {
179                 $circulator->do_checkout();
180
181         } elsif( $api =~ /checkin/ ) {
182                 $circulator->do_checkin();
183
184         } elsif( $api =~ /renew/ ) {
185                 $circulator->is_renewal(1);
186                 $circulator->do_renew();
187         }
188
189         if( $circulator->bail_out ) {
190
191                 my @ee;
192                 # make sure no success event accidentally slip in
193                 $circulator->events(
194                         [ grep { $_->{textcode} ne 'SUCCESS' } @{$circulator->events} ]);
195                 my @e = @{$circulator->events};
196                 push( @ee, $_->{textcode} ) for @e;
197                 $logger->info("circulator: bailing out with events: @ee");
198                 $circulator->editor->rollback;
199
200         } else {
201                 $circulator->editor->commit;
202         }
203
204         $circulator->script_runner->cleanup;
205         
206         return circ_events($circulator);
207 }
208
209 sub circ_events {
210         my $circ = shift;
211         my @e = @{$circ->events};
212         return (@e == 1) ? $e[0] : \@e;
213 }
214
215
216
217 sub translate_legacy_args {
218         my $args = shift;
219
220         if( $$args{barcode} ) {
221                 $$args{copy_barcode} = $$args{barcode};
222                 delete $$args{barcode};
223         }
224
225         if( $$args{copyid} ) {
226                 $$args{copy_id} = $$args{copyid};
227                 delete $$args{copyid};
228         }
229
230         if( $$args{patronid} ) {
231                 $$args{patron_id} = $$args{patronid};
232                 delete $$args{patronid};
233         }
234
235         if( $$args{patron} and !ref($$args{patron}) ) {
236                 $$args{patron_id} = $$args{patron};
237                 delete $$args{patron};
238         }
239
240
241         if( $$args{noncat} ) {
242                 $$args{is_noncat} = $$args{noncat};
243                 delete $$args{noncat};
244         }
245
246         if( $$args{precat} ) {
247                 $$args{is_precat} = $$args{precat};
248                 delete $$args{precat};
249         }
250 }
251
252
253
254 # --------------------------------------------------------------------------
255 # This package actually manages all of the circulation logic
256 # --------------------------------------------------------------------------
257 package OpenILS::Application::Circ::Circulator;
258 use strict; use warnings;
259 use vars q/$AUTOLOAD/;
260 use DateTime;
261 use OpenILS::Utils::Fieldmapper;
262 use OpenSRF::Utils::Cache;
263 use Digest::MD5 qw(md5_hex);
264 use DateTime::Format::ISO8601;
265 use OpenILS::Utils::PermitHold;
266 use OpenSRF::Utils qw/:datetime/;
267 use OpenSRF::Utils::SettingsClient;
268 use OpenILS::Application::Circ::Holds;
269 use OpenILS::Application::Circ::Transit;
270 use OpenSRF::Utils::Logger qw(:logger);
271 use OpenILS::Utils::CStoreEditor qw/:funcs/;
272 use OpenILS::Application::Circ::ScriptBuilder;
273 use OpenILS::Const qw/:const/;
274
275 my $U                           = "OpenILS::Application::AppUtils";
276 my $holdcode    = "OpenILS::Application::Circ::Holds";
277 my $transcode   = "OpenILS::Application::Circ::Transit";
278
279 sub DESTROY { }
280
281
282 # --------------------------------------------------------------------------
283 # Add a pile of automagic getter/setter methods
284 # --------------------------------------------------------------------------
285 my @AUTOLOAD_FIELDS = qw/
286         penalty_request
287         remote_hold
288         backdate
289         copy
290         copy_id
291         copy_barcode
292         patron
293         patron_id
294         patron_barcode
295         script_runner
296         volume
297         title
298         is_renewal
299         is_noncat
300         is_precat
301         is_checkin
302         noncat_type
303         editor
304         events
305         cache_handle
306         override
307         circ_permit_patron
308         circ_permit_copy
309         circ_duration
310         circ_recurring_fines
311         circ_max_fines
312         circ_permit_renew
313         circ
314         transit
315         hold
316         permit_key
317         noncat_circ_lib
318         noncat_count
319         checkout_time
320         dummy_title
321         dummy_author
322         circ_lib
323         barcode
324    duration_level
325    recurring_fines_level
326    duration_rule
327    recurring_fines_rule
328    max_fine_rule
329         renewal_remaining
330         due_date
331         fulfilled_holds
332         transit
333         checkin_changed
334         force
335         old_circ
336         permit_override
337 /;
338
339
340 sub AUTOLOAD {
341         my $self = shift;
342         my $type = ref($self) or die "$self is not an object";
343         my $data = shift;
344         my $name = $AUTOLOAD;
345         $name =~ s/.*://o;   
346
347         unless (grep { $_ eq $name } @AUTOLOAD_FIELDS) {
348                 $logger->error("circulator: $type: invalid autoload field: $name");
349                 die "$type: invalid autoload field: $name\n" 
350         }
351
352         {
353                 no strict 'refs';
354                 *{"${type}::${name}"} = sub {
355                         my $s = shift;
356                         my $v = shift;
357                         $s->{$name} = $v if defined $v;
358                         return $s->{$name};
359                 }
360         }
361         return $self->$name($data);
362 }
363
364
365 sub new {
366         my( $class, $auth, %args ) = @_;
367         $class = ref($class) || $class;
368         my $self = bless( {}, $class );
369
370         $self->events([]);
371         $self->editor( 
372                 new_editor(xact => 1, authtoken => $auth) );
373
374         unless( $self->editor->checkauth ) {
375                 $self->bail_on_events($self->editor->event);
376                 return $self;
377         }
378
379         $self->cache_handle(OpenSRF::Utils::Cache->new('global'));
380
381         $self->$_($args{$_}) for keys %args;
382
383         $self->circ_lib(
384                 ($self->circ_lib) ? $self->circ_lib : $self->editor->requestor->ws_ou);
385
386         return $self;
387 }
388
389
390 # --------------------------------------------------------------------------
391 # True if we should discontinue processing
392 # --------------------------------------------------------------------------
393 sub bail_out {
394         my( $self, $bool ) = @_;
395         if( defined $bool ) {
396                 $logger->info("circulator: BAILING OUT") if $bool;
397                 $self->{bail_out} = $bool;
398         }
399         return $self->{bail_out};
400 }
401
402
403 sub push_events {
404         my( $self, @evts ) = @_;
405         for my $e (@evts) {
406                 next unless $e;
407                 $logger->info("circulator: pushing event ".$e->{textcode});
408                 push( @{$self->events}, $e ) unless
409                         grep { $_->{textcode} eq $e->{textcode} } @{$self->events};
410         }
411 }
412
413 sub mk_permit_key {
414         my $self = shift;
415         my $key = md5_hex( time() . rand() . "$$" );
416         $self->cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
417         return $self->permit_key($key);
418 }
419
420 sub check_permit_key {
421         my $self = shift;
422         my $key = $self->permit_key;
423         return 0 unless $key;
424         my $k = "oils_permit_key_$key";
425         my $one = $self->cache_handle->get_cache($k);
426         $self->cache_handle->delete_cache($k);
427         return ($one) ? 1 : 0;
428 }
429
430
431 # --------------------------------------------------------------------------
432 # This builds the script runner environment and fetches most of the
433 # objects we need
434 # --------------------------------------------------------------------------
435 sub mk_script_runner {
436         my $self = shift;
437         my $args = {};
438
439         $args->{ignore_user_status} = 1 if $self->is_checkin;
440
441         my @fields = 
442                 qw/copy copy_barcode copy_id patron 
443                         patron_id patron_barcode volume title editor/;
444
445         # Translate our objects into the ScriptBuilder args hash
446         $$args{$_} = $self->$_() for @fields;
447         $$args{fetch_patron_by_circ_copy} = 1;
448         $$args{fetch_patron_circ_info} = 1;
449
450         # This fetches most of the objects we need
451         $self->script_runner(
452                 OpenILS::Application::Circ::ScriptBuilder->build($args));
453
454         # Now we translate the ScriptBuilder objects back into self
455         $self->$_($$args{$_}) for @fields;
456
457         my @evts = @{$args->{_events}} if $args->{_events};
458
459         $logger->debug("circulator: script builder returned events: : @evts") if @evts;
460
461
462         if(@evts) {
463                 # Anything besides ASSET_COPY_NOT_FOUND will stop processing
464                 if(!$self->is_noncat and 
465                         @evts == 1 and 
466                         $evts[0]->{textcode} eq 'ASSET_COPY_NOT_FOUND') {
467                                 $self->is_precat(1);
468
469                 } else {
470                         my @e = grep { $_->{textcode} ne 'ASSET_COPY_NOT_FOUND' } @evts;
471                         return $self->bail_on_events(@e);
472                 }
473         }
474
475         $self->is_precat(1) if $self->copy and $self->copy->call_number == OILS_PRECAT_CALL_NUMBER;
476
477         # We can't renew if there is no copy
478         return $self->bail_on_events(@evts) if 
479                 $self->is_renewal and !$self->copy;
480
481         # Set some circ-specific flags in the script environment
482         my $evt = "environment";
483         $self->script_runner->insert("$evt.isRenewal", ($self->is_renewal) ? 1 : undef);
484
485         if( $self->is_noncat ) {
486       $self->script_runner->insert("$evt.isNonCat", 1);
487       $self->script_runner->insert("$evt.nonCatType", $self->noncat_type);
488         }
489
490         if( $self->is_precat ) {
491                 $self->script_runner->insert("environment.isPrecat", 1, 1);
492         }
493
494         $self->script_runner->add_path( $_ ) for @$script_libs;
495
496         return 1;
497 }
498
499
500
501
502 # --------------------------------------------------------------------------
503 # Does the circ permit work
504 # --------------------------------------------------------------------------
505 sub do_permit {
506         my $self = shift;
507
508         $self->log_me("do_permit()");
509
510         unless( $self->editor->requestor->id == $self->patron->id ) {
511                 return $self->bail_on_events($self->editor->event)
512                         unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
513         }
514
515
516         $self->check_captured_holds();
517         $self->do_copy_checks();
518         return if $self->bail_out;
519         $self->run_patron_permit_scripts();
520         $self->run_copy_permit_scripts() 
521                 unless $self->is_precat or $self->is_noncat;
522         $self->override_events() unless $self->is_renewal;
523         return if $self->bail_out;
524
525         if( $self->is_precat ) {
526                 $self->push_events(
527                         OpenILS::Event->new(
528                                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
529                 return $self->bail_out(1) unless $self->is_renewal;
530         }
531
532         $self->push_events(
533       OpenILS::Event->new(
534                         'SUCCESS', 
535                         payload => $self->mk_permit_key));
536 }
537
538
539 sub check_captured_holds {
540    my $self    = shift;
541    my $copy    = $self->copy;
542    my $patron  = $self->patron;
543
544         return undef unless $copy;
545
546         my $s = $U->copy_status($copy->status)->id;
547         return unless $s == OILS_COPY_STATUS_ON_HOLDS_SHELF;
548         $logger->info("circulator: copy is on holds shelf, searching for the correct hold");
549
550         # Item is on the holds shelf, make sure it's going to the right person
551         my $holds       = $self->editor->search_action_hold_request(
552                 [
553                         { 
554                                 current_copy            => $copy->id , 
555                                 capture_time            => { '!=' => undef },
556                                 cancel_time                     => undef, 
557                                 fulfillment_time        => undef 
558                         },
559                         { limit => 1 }
560                 ]
561         );
562
563         if( $holds and $$holds[0] ) {
564                 return undef if $$holds[0]->usr == $patron->id;
565         }
566
567         $logger->info("circulator: this copy is needed by a different patron to fulfill a hold");
568
569         $self->push_events(OpenILS::Event->new('ITEM_ON_HOLDS_SHELF'));
570 }
571
572
573 sub do_copy_checks {
574         my $self = shift;
575         my $copy = $self->copy;
576         return unless $copy;
577
578         my $stat = $U->copy_status($copy->status)->id;
579
580         # We cannot check out a copy if it is in-transit
581         if( $stat == OILS_COPY_STATUS_IN_TRANSIT ) {
582                 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
583         }
584
585         $self->handle_claims_returned();
586         return if $self->bail_out;
587
588         # no claims returned circ was found, check if there is any open circ
589         unless( $self->is_renewal ) {
590                 my $circs = $self->editor->search_action_circulation(
591                         { target_copy => $copy->id, checkin_time => undef }
592                 );
593
594                 return $self->bail_on_events(
595                         OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
596         }
597 }
598
599
600 sub send_penalty_request {
601         my $self = shift;
602         my $ses = OpenSRF::AppSession->create('open-ils.penalty');
603         $self->penalty_request(
604                 $ses->request(  
605                         'open-ils.penalty.patron_penalty.calculate', 
606                         {       update => 1, 
607                                 authtoken => $self->editor->authtoken,
608                                 patron => $self->patron } ) );
609 }
610
611 sub gather_penalty_request {
612         my $self = shift;
613         return [] unless $self->penalty_request;
614         my $data = $self->penalty_request->recv;
615         if( ref $data ) {
616                 $data = $data->content;
617                 return $data->{fatal_penalties};
618         }
619         $logger->error("circulator: penalty request returned no data");
620         return [];
621 }
622
623 # ---------------------------------------------------------------------
624 # This pushes any patron-related events into the list but does not
625 # set bail_out for any events
626 # ---------------------------------------------------------------------
627 sub run_patron_permit_scripts {
628         my $self                = shift;
629         my $runner              = $self->script_runner;
630         my $patronid    = $self->patron->id;
631
632         $self->send_penalty_request();
633
634         # ---------------------------------------------------------------------
635         # Now run the patron permit script 
636         # ---------------------------------------------------------------------
637         $runner->load($self->circ_permit_patron);
638         my $result = $runner->run or 
639                 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
640
641         my $patron_events = $result->{events};
642         my @allevents; 
643
644         my $penalties = $self->gather_penalty_request();
645         push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
646
647         $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
648
649         $self->push_events(@allevents);
650 }
651
652
653 sub run_copy_permit_scripts {
654         my $self = shift;
655         my $copy = $self->copy || return;
656         my $runner = $self->script_runner;
657         
658    # ---------------------------------------------------------------------
659    # Capture all of the copy permit events
660    # ---------------------------------------------------------------------
661    $runner->load($self->circ_permit_copy);
662    my $result = $runner->run or 
663                 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
664    my $copy_events = $result->{events};
665
666    # ---------------------------------------------------------------------
667    # Now collect all of the events together
668    # ---------------------------------------------------------------------
669         my @allevents;
670    push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
671
672         # See if this copy has an alert message
673         my $ae = $self->check_copy_alert();
674         push( @allevents, $ae ) if $ae;
675
676    # uniquify the events
677    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
678    @allevents = values %hash;
679
680    for (@allevents) {
681       $_->{payload} = $copy if 
682                         ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
683    }
684
685         $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
686
687         $self->push_events(@allevents);
688 }
689
690
691 sub check_copy_alert {
692         my $self = shift;
693         return OpenILS::Event->new(
694                 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
695                 if $self->copy and $self->copy->alert_message;
696         return undef;
697 }
698
699
700
701 # --------------------------------------------------------------------------
702 # If the call is overriding and has permissions to override every collected
703 # event, the are cleared.  Any event that the caller does not have
704 # permission to override, will be left in the event list and bail_out will
705 # be set
706 # XXX We need code in here to cancel any holds/transits on copies 
707 # that are being force-checked out
708 # --------------------------------------------------------------------------
709 sub override_events {
710         my $self = shift;
711         my @events = @{$self->events};
712         return unless @events;
713
714         if(!$self->override) {
715                 return $self->bail_out(1) 
716                         if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
717         }       
718
719         $self->events([]);
720         
721    for my $e (@events) {
722       my $tc = $e->{textcode};
723       next if $tc eq 'SUCCESS';
724       my $ov = "$tc.override";
725       $logger->info("circulator: attempting to override event: $ov");
726
727                 return $self->bail_on_events($self->editor->event)
728                         unless( $self->editor->allowed($ov)     );
729    }
730 }
731         
732
733 # --------------------------------------------------------------------------
734 # If there is an open claimsreturn circ on the requested copy, close the 
735 # circ if overriding, otherwise bail out
736 # --------------------------------------------------------------------------
737 sub handle_claims_returned {
738         my $self = shift;
739         my $copy = $self->copy;
740
741         my $CR = $self->editor->search_action_circulation(
742                 {       
743                         target_copy             => $copy->id,
744                         stop_fines              => OILS_STOP_FINES_CLAIMSRETURNED,
745                         checkin_time    => undef,
746                 }
747         );
748
749         return unless ($CR = $CR->[0]); 
750
751         my $evt;
752
753         # - If the caller has set the override flag, we will check the item in
754         if($self->override) {
755
756                 $CR->checkin_time('now');       
757                 $CR->checkin_lib($self->editor->requestor->ws_ou);
758                 $CR->checkin_staff($self->editor->requestor->id);
759
760                 $evt = $self->editor->event 
761                         unless $self->editor->update_action_circulation($CR);
762
763         } else {
764                 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
765         }
766
767         $self->bail_on_events($evt) if $evt;
768         return;
769 }
770
771
772 # --------------------------------------------------------------------------
773 # This performs the checkout
774 # --------------------------------------------------------------------------
775 sub do_checkout {
776         my $self = shift;
777
778         $self->log_me("do_checkout()");
779
780         # make sure perms are good if this isn't a renewal
781         unless( $self->is_renewal ) {
782                 return $self->bail_on_events($self->editor->event)
783                         unless( $self->editor->allowed('COPY_CHECKOUT') );
784         }
785
786         # verify the permit key
787         unless( $self->check_permit_key ) {
788                 if( $self->permit_override ) {
789                         return $self->bail_on_events($self->editor->event)
790                                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
791                 } else {
792                         return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
793                 }       
794         }
795
796         # if this is a non-cataloged circ, build the circ and finish
797         if( $self->is_noncat ) {
798                 $self->checkout_noncat;
799                 $self->push_events(
800                         OpenILS::Event->new('SUCCESS', 
801                         payload => { noncat_circ => $self->circ }));
802                 return;
803         }
804
805         if( $self->is_precat ) {
806                 $self->script_runner->insert("environment.isPrecat", 1, 1);
807                 $self->make_precat_copy;
808                 return if $self->bail_out;
809
810         } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
811                 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
812         }
813
814         $self->do_copy_checks;
815         return if $self->bail_out;
816
817         $self->run_checkout_scripts();
818         return if $self->bail_out;
819
820         $self->build_checkout_circ_object();
821         return if $self->bail_out;
822
823         $self->apply_modified_due_date();
824         return if $self->bail_out;
825
826         return $self->bail_on_events($self->editor->event)
827                 unless $self->editor->create_action_circulation($self->circ);
828
829         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
830         $self->update_copy;
831         return if $self->bail_out;
832
833         $self->handle_checkout_holds();
834         return if $self->bail_out;
835
836    # ------------------------------------------------------------------------------
837    # Update the patron penalty info in the DB
838    # ------------------------------------------------------------------------------
839    $U->update_patron_penalties(
840       authtoken => $self->editor->authtoken,
841       patron    => $self->patron,
842       background  => 1,
843    );
844
845         my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
846         $self->push_events(
847                 OpenILS::Event->new('SUCCESS',
848                         payload  => {
849                                 copy              => $U->unflesh_copy($self->copy),
850                                 circ              => $self->circ,
851                                 record            => $record,
852                                 holds_fulfilled   => $self->fulfilled_holds,
853                         }
854                 )
855         );
856 }
857
858 sub update_copy {
859         my $self = shift;
860         my $copy = $self->copy;
861
862         my $stat = $copy->status if ref $copy->status;
863         my $loc = $copy->location if ref $copy->location;
864         my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
865
866         $copy->status($stat->id) if $stat;
867         $copy->location($loc->id) if $loc;
868         $copy->circ_lib($circ_lib->id) if $circ_lib;
869         $copy->editor($self->editor->requestor->id);
870         $copy->edit_date('now');
871         $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
872
873         return $self->bail_on_events($self->editor->event)
874                 unless $self->editor->update_asset_copy($self->copy);
875
876         $copy->status($U->copy_status($copy->status));
877         $copy->location($loc) if $loc;
878         $copy->circ_lib($circ_lib) if $circ_lib;
879 }
880
881
882 sub bail_on_events {
883         my( $self, @evts ) = @_;
884         $self->push_events(@evts);
885         $self->bail_out(1);
886 }
887
888 sub handle_checkout_holds {
889    my $self    = shift;
890
891    my $copy    = $self->copy;
892    my $patron  = $self->patron;
893
894         my $holds       = $self->editor->search_action_hold_request(
895                 { 
896                         current_copy            => $copy->id , 
897                         cancel_time                     => undef, 
898                         fulfillment_time        => undef 
899                 }
900         );
901
902    my @fulfilled;
903
904    # XXX We should only fulfill one hold here...
905    # XXX If a hold was transited to the user who is checking out
906    # the item, we need to make sure that hold is what's grabbed
907    if(@$holds) {
908
909       # for now, just sort by id to get what should be the oldest hold
910       $holds = [ sort { $a->id <=> $b->id } @$holds ];
911       my @myholds = grep { $_->usr eq $patron->id } @$holds;
912       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
913
914       if(@myholds) {
915          my $hold = $myholds[0];
916
917          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
918
919          # if the hold was never officially captured, capture it.
920          $hold->capture_time('now') unless $hold->capture_time;
921
922                         # just make sure it's set correctly
923          $hold->current_copy($copy->id); 
924
925          $hold->fulfillment_time('now');
926                         $hold->fulfillment_staff($self->editor->requestor->id);
927                         $hold->fulfillment_lib($self->editor->requestor->ws_ou);
928
929                         return $self->bail_on_events($self->editor->event)
930                                 unless $self->editor->update_action_hold_request($hold);
931
932                         $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
933
934          push( @fulfilled, $hold->id );
935       }
936
937       # If there are any holds placed for other users that point to this copy,
938       # then we need to un-target those holds so the targeter can pick a new copy
939       for(@altholds) {
940
941          $logger->info("circulator: un-targeting hold ".$_->id.
942             " because copy ".$copy->id." is getting checked out");
943
944                         # - make the targeter process this hold at next run
945          $_->clear_prev_check_time; 
946
947                         # - clear out the targetted copy
948          $_->clear_current_copy;
949          $_->clear_capture_time;
950
951                         return $self->bail_on_event($self->editor->event)
952                                 unless $self->editor->update_action_hold_request($_);
953       }
954    }
955
956         $self->fulfilled_holds(\@fulfilled);
957 }
958
959
960
961 sub run_checkout_scripts {
962         my $self = shift;
963
964         my $evt;
965    my $runner = $self->script_runner;
966    $runner->load($self->circ_duration);
967
968    my $result = $runner->run or 
969                 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
970
971    my $duration   = $result->{durationRule};
972    my $recurring  = $result->{recurringFinesRule};
973    my $max_fine   = $result->{maxFine};
974
975         if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
976
977                 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
978                 return $self->bail_on_events($evt) if $evt;
979         
980                 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
981                 return $self->bail_on_events($evt) if $evt;
982         
983                 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
984                 return $self->bail_on_events($evt) if $evt;
985
986         } else {
987
988                 # The item circulates with an unlimited duration
989                 $duration       = undef;
990                 $recurring      = undef;
991                 $max_fine       = undef;
992         }
993
994    $self->duration_rule($duration);
995    $self->recurring_fines_rule($recurring);
996    $self->max_fine_rule($max_fine);
997 }
998
999
1000 sub build_checkout_circ_object {
1001         my $self = shift;
1002
1003    my $circ       = Fieldmapper::action::circulation->new;
1004    my $duration   = $self->duration_rule;
1005    my $max        = $self->max_fine_rule;
1006    my $recurring  = $self->recurring_fines_rule;
1007    my $copy       = $self->copy;
1008    my $patron     = $self->patron;
1009
1010         if( $duration ) {
1011
1012                 my $dname = $duration->name;
1013                 my $mname = $max->name;
1014                 my $rname = $recurring->name;
1015         
1016                 $logger->debug("circulator: building circulation ".
1017                         "with duration=$dname, maxfine=$mname, recurring=$rname");
1018         
1019                 $circ->duration( $duration->shrt ) 
1020                         if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1021                 $circ->duration( $duration->normal ) 
1022                         if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1023                 $circ->duration( $duration->extended ) 
1024                         if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1025         
1026                 $circ->recuring_fine( $recurring->low ) 
1027                         if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1028                 $circ->recuring_fine( $recurring->normal ) 
1029                         if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1030                 $circ->recuring_fine( $recurring->high ) 
1031                         if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1032
1033                 $circ->duration_rule( $duration->name );
1034                 $circ->recuring_fine_rule( $recurring->name );
1035                 $circ->max_fine_rule( $max->name );
1036                 $circ->max_fine( $max->amount );
1037
1038                 $circ->fine_interval($recurring->recurance_interval);
1039                 $circ->renewal_remaining( $duration->max_renewals );
1040
1041         } else {
1042
1043                 $logger->info("circulator: copy found with an unlimited circ duration");
1044                 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1045                 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1046                 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1047                 $circ->renewal_remaining(0);
1048         }
1049
1050    $circ->target_copy( $copy->id );
1051    $circ->usr( $patron->id );
1052    $circ->circ_lib( $self->circ_lib );
1053
1054    if( $self->is_renewal ) {
1055       $circ->opac_renewal(1);
1056       $circ->renewal_remaining($self->renewal_remaining);
1057       $circ->circ_staff($self->editor->requestor->id);
1058    }
1059
1060    # if the user provided an overiding checkout time,
1061    # (e.g. the checkout really happened several hours ago), then
1062    # we apply that here.  Does this need a perm??
1063         $circ->xact_start(clense_ISO8601($self->checkout_time))
1064                 if $self->checkout_time;
1065
1066    # if a patron is renewing, 'requestor' will be the patron
1067    $circ->circ_staff($self->editor->requestor->id);
1068         $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1069
1070         $self->circ($circ);
1071 }
1072
1073
1074 sub apply_modified_due_date {
1075         my $self = shift;
1076         my $circ = $self->circ;
1077         my $copy = $self->copy;
1078
1079    if( $self->due_date ) {
1080
1081                 return $self->bail_on_events($self->editor->event)
1082                         unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1083
1084       $circ->due_date(clense_ISO8601($self->due_date));
1085
1086    } else {
1087
1088       # if the due_date lands on a day when the location is closed
1089       return unless $copy and $circ->due_date;
1090
1091                 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1092
1093       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1094                         " with an item due date of ".$circ->due_date );
1095
1096       my $dateinfo = $U->storagereq(
1097          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1098                         $org, $circ->due_date );
1099
1100       if($dateinfo) {
1101          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1102             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1103
1104             # XXX make the behavior more dynamic
1105             # for now, we just push the due date to after the close date
1106             $circ->due_date($dateinfo->{end});
1107       }
1108    }
1109 }
1110
1111
1112
1113 sub create_due_date {
1114         my( $self, $duration ) = @_;
1115    my ($sec,$min,$hour,$mday,$mon,$year) =
1116       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1117    $year += 1900; $mon += 1;
1118    my $due_date = sprintf(
1119       '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1120       $year, $mon, $mday, $hour, $min, $sec);
1121    return $due_date;
1122 }
1123
1124
1125
1126 sub make_precat_copy {
1127         my $self = shift;
1128         my $copy = $self->copy;
1129
1130    if($copy) {
1131       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1132
1133       $copy->editor($self->editor->requestor->id);
1134       $copy->edit_date('now');
1135       $copy->dummy_title($self->dummy_title);
1136       $copy->dummy_author($self->dummy_author);
1137
1138                 $self->update_copy();
1139                 return;
1140    }
1141
1142    $logger->info("circulator: Creating a new precataloged ".
1143                 "copy in checkout with barcode " . $self->copy_barcode);
1144
1145    $copy = Fieldmapper::asset::copy->new;
1146    $copy->circ_lib($self->circ_lib);
1147    $copy->creator($self->editor->requestor->id);
1148    $copy->editor($self->editor->requestor->id);
1149    $copy->barcode($self->copy_barcode);
1150    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1151    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1152    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1153
1154    $copy->dummy_title($self->dummy_title || "");
1155    $copy->dummy_author($self->dummy_author || "");
1156
1157         unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1158                 $self->bail_out(1);
1159                 $self->push_events($self->editor->event);
1160                 return;
1161         }       
1162
1163         # this is a little bit of a hack, but we need to 
1164         # get the copy into the script runner
1165         $self->script_runner->insert("environment.copy", $copy, 1);
1166 }
1167
1168
1169 sub checkout_noncat {
1170         my $self = shift;
1171
1172         my $circ;
1173         my $evt;
1174
1175    my $lib              = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1176    my $count    = $self->noncat_count || 1;
1177    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1178
1179    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1180
1181    for(1..$count) {
1182
1183       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1184          $self->editor->requestor->id, 
1185                         $self->patron->id, 
1186                         $lib, 
1187                         $self->noncat_type, 
1188                         $cotime,
1189                         $self->editor );
1190
1191                 if( $evt ) {
1192                         $self->push_events($evt);
1193                         $self->bail_out(1);
1194                         return; 
1195                 }
1196                 $self->circ($circ);
1197    }
1198 }
1199
1200
1201 sub do_checkin {
1202         my $self = shift;
1203         $self->log_me("do_checkin()");
1204
1205
1206         return $self->bail_on_events(
1207                 OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1208                 unless $self->copy;
1209
1210         if( $self->checkin_check_holds_shelf() ) {
1211                 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1212                 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1213                 $self->checkin_flesh_events;
1214                 return;
1215         }
1216
1217         unless( $self->is_renewal ) {
1218                 return $self->bail_on_events($self->editor->event)
1219                         unless $self->editor->allowed('COPY_CHECKIN');
1220         }
1221
1222         $self->push_events($self->check_copy_alert());
1223         $self->push_events($self->check_checkin_copy_status());
1224
1225         # the renew code will have already found our circulation object
1226         unless( $self->is_renewal and $self->circ ) {
1227                 $self->circ(
1228                         $self->editor->search_action_circulation(
1229                         { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1230         }
1231
1232         # if the circ is marked as 'claims returned', add the event to the list
1233         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1234                 if ($self->circ and $self->circ->stop_fines 
1235                                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1236
1237         # handle the overridable events 
1238         $self->override_events unless $self->is_renewal;
1239         return if $self->bail_out;
1240         
1241         if( $self->copy ) {
1242                 $self->transit(
1243                         $self->editor->search_action_transit_copy(
1244                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1245         }
1246
1247         if( $self->circ ) {
1248                 $self->checkin_handle_circ;
1249                 return if $self->bail_out;
1250                 $self->checkin_changed(1);
1251
1252         } elsif( $self->transit ) {
1253                 my $hold_transit = $self->process_received_transit;
1254                 $self->checkin_changed(1);
1255
1256                 if( $self->bail_out ) { 
1257                         $self->checkin_flesh_events;
1258                         return;
1259                 }
1260                 
1261                 if( my $e = $self->check_checkin_copy_status() ) {
1262                         # If the original copy status is special, alert the caller
1263                         my $ev = $self->events;
1264                         $self->events([$e]);
1265                         $self->override_events;
1266                         return if $self->bail_out;
1267                         $self->events($ev);
1268                 }
1269
1270                 if( $hold_transit or 
1271                                 $U->copy_status($self->copy->status)->id 
1272                                         == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1273                         $self->hold(
1274                                 ($hold_transit) ?
1275                                         $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1276                                         $U->fetch_open_hold_by_copy($self->copy->id)
1277                                 );
1278
1279                         $self->checkin_flesh_events;
1280                         return;
1281                 } 
1282         }
1283
1284         if( $self->is_renewal ) {
1285                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1286                 return;
1287         }
1288
1289    # ------------------------------------------------------------------------------
1290    # Circulations and transits are now closed where necessary.  Now go on to see if
1291    # this copy can fulfill a hold or needs to be routed to a different location
1292    # ------------------------------------------------------------------------------
1293
1294         if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1295                 return if $self->bail_out;
1296
1297    } else { # not needed for a hold
1298
1299
1300                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1301                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1302
1303                 if( $self->remote_hold ) {
1304                         $circ_lib = $self->remote_hold->pickup_lib;
1305                         $logger->warn("circulator: Copy ".$self->copy->barcode.
1306                                 " is on a remote hold's shelf, sending to $circ_lib");
1307                 }
1308
1309                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1310
1311       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1312
1313                         $self->checkin_handle_precat();
1314                         return if $self->bail_out;
1315
1316       } else {
1317
1318                         my $bc = $self->copy->barcode;
1319                         $logger->info("circulator: copy $bc at a remote lib  - sending home");
1320                         $self->checkin_build_copy_transit();
1321                         return if $self->bail_out;
1322                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1323       }
1324    }
1325
1326         $self->reshelve_copy;
1327         return if $self->bail_out;
1328
1329         unless($self->checkin_changed) {
1330
1331                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1332                 my $stat = $U->copy_status($self->copy->status)->id;
1333
1334         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1335          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1336                 $self->bail_out(1); # no need to commit anything
1337
1338         } else {
1339                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1340                         unless @{$self->events};
1341         }
1342
1343         $self->checkin_flesh_events;
1344         return;
1345 }
1346
1347 sub reshelve_copy {
1348    my $self    = shift;
1349    my $force   = $self->force || shift;
1350    my $copy    = $self->copy;
1351
1352    my $stat = $U->copy_status($copy->status)->id;
1353
1354    if($force || (
1355       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1356       $stat != OILS_COPY_STATUS_CATALOGING and
1357       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1358       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1359
1360         $copy->status( OILS_COPY_STATUS_RESHELVING );
1361                         $self->update_copy;
1362                         $self->checkin_changed(1);
1363         }
1364 }
1365
1366
1367 # Returns true if the item is at the current location
1368 # because it was transited there for a hold and the 
1369 # hold has not been fulfilled
1370 sub checkin_check_holds_shelf {
1371         my $self = shift;
1372         return 0 unless $self->copy;
1373
1374         return 0 unless 
1375                 $U->copy_status($self->copy->status)->id ==
1376                         OILS_COPY_STATUS_ON_HOLDS_SHELF;
1377
1378         # find the hold that put us on the holds shelf
1379         my $holds = $self->editor->search_action_hold_request(
1380                 { 
1381                         current_copy => $self->copy->id,
1382                         capture_time => { '!=' => undef },
1383                         fulfillment_time => undef,
1384                         cancel_time => undef,
1385                 }
1386         );
1387
1388         return 0 unless @$holds;
1389
1390         my $hold = $$holds[0];
1391
1392         $logger->info("circulator: we found a captured, un-fulfilled hold [".
1393                 $hold->id. "] for copy ".$self->copy->barcode);
1394
1395         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1396                 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1397                 return 1;
1398         }
1399
1400         $logger->info("circulator: hold is not for here..");
1401         $self->remote_hold($hold);
1402         return 0;
1403 }
1404
1405
1406 sub checkin_handle_precat {
1407         my $self        = shift;
1408    my $copy    = $self->copy;
1409
1410    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1411       $copy->status(OILS_COPY_STATUS_CATALOGING);
1412                 $self->update_copy();
1413                 $self->checkin_changed(1);
1414                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1415    }
1416 }
1417
1418
1419 sub checkin_build_copy_transit {
1420         my $self                        = shift;
1421         my $copy       = $self->copy;
1422    my $transit    = Fieldmapper::action::transit_copy->new;
1423
1424    $transit->source($self->editor->requestor->ws_ou);
1425    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1426    $transit->target_copy($copy->id);
1427    $transit->source_send_time('now');
1428    $transit->copy_status( $U->copy_status($copy->status)->id );
1429
1430         $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1431
1432         return $self->bail_on_events($self->editor->event)
1433                 unless $self->editor->create_action_transit_copy($transit);
1434
1435    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1436         $self->update_copy;
1437         $self->checkin_changed(1);
1438 }
1439
1440
1441 sub attempt_checkin_hold_capture {
1442         my $self = shift;
1443         my $copy = $self->copy;
1444
1445         # See if this copy can fulfill any holds
1446         my ($hold) = $holdcode->find_nearest_permitted_hold(
1447                 OpenSRF::AppSession->create('open-ils.storage'), 
1448                 $copy, $self->editor->requestor );
1449
1450         if(!$hold) {
1451                 $logger->debug("circulator: no potential permitted".
1452                         "holds found for copy ".$copy->barcode);
1453                 return undef;
1454         }
1455
1456
1457         $logger->info("circulator: found permitted hold ".
1458                 $hold->id . " for copy, capturing...");
1459
1460         $hold->current_copy($copy->id);
1461         $hold->capture_time('now');
1462
1463         # prevent DB errors caused by fetching 
1464         # holds from storage, and updating through cstore
1465         $hold->clear_fulfillment_time;
1466         $hold->clear_fulfillment_staff;
1467         $hold->clear_fulfillment_lib;
1468         $hold->clear_expire_time; 
1469         $hold->clear_cancel_time;
1470         $hold->clear_prev_check_time unless $hold->prev_check_time;
1471
1472         $self->bail_on_events($self->editor->event)
1473                 unless $self->editor->update_action_hold_request($hold);
1474         $self->hold($hold);
1475         $self->checkin_changed(1);
1476
1477         return 1 if $self->bail_out;
1478
1479         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1480
1481                 # This hold was captured in the correct location
1482         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1483                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1484
1485                 $self->do_hold_notify($hold->id);
1486
1487         } else {
1488         
1489                 # Hold needs to be picked up elsewhere.  Build a hold
1490                 # transit and route the item.
1491                 $self->checkin_build_hold_transit();
1492         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1493                 return 1 if $self->bail_out;
1494                 $self->push_events(
1495                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1496         }
1497
1498         # make sure we save the copy status
1499         $self->update_copy;
1500         return 1;
1501 }
1502
1503 sub do_hold_notify {
1504         my( $self, $holdid ) = @_;
1505         my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1506                 editor => $self->editor, hold_id => $holdid );
1507
1508         if(!$notifier->event) {
1509
1510                 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1511
1512                 my $stat = $notifier->send_email_notify;
1513                 $logger->info("ciculator: hold notify succeeded for hold $holdid") if $stat eq '1';
1514                 $logger->warn("ciculator:  * hold notify failed for hold $holdid") if $stat ne '1';
1515
1516         } else {
1517                 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1518         }
1519 }
1520
1521
1522 sub checkin_build_hold_transit {
1523         my $self = shift;
1524
1525
1526    my $copy = $self->copy;
1527    my $hold = $self->hold;
1528    my $trans = Fieldmapper::action::hold_transit_copy->new;
1529
1530         $logger->debug("circulator: building hold transit for ".$copy->barcode);
1531
1532    $trans->hold($hold->id);
1533    $trans->source($self->editor->requestor->ws_ou);
1534    $trans->dest($hold->pickup_lib);
1535    $trans->source_send_time("now");
1536    $trans->target_copy($copy->id);
1537
1538         # when the copy gets to its destination, it will recover
1539         # this status - put it onto the holds shelf
1540    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1541
1542         return $self->bail_on_events($self->editor->event)
1543                 unless $self->editor->create_action_hold_transit_copy($trans);
1544 }
1545
1546
1547
1548 sub process_received_transit {
1549         my $self = shift;
1550         my $copy = $self->copy;
1551    my $copyid = $self->copy->id;
1552
1553         my $status_name = $U->copy_status($copy->status)->name;
1554    $logger->debug("circulator: attempting transit receive on ".
1555                 "copy $copyid. Copy status is $status_name");
1556
1557         my $transit = $self->transit;
1558
1559    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1560       $logger->info("circulator: Fowarding transit on copy which is destined ".
1561          "for a different location. copy=$copyid,current ".
1562          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1563
1564                 return $self->bail_on_events(
1565                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1566    }
1567
1568    # The transit is received, set the receive time
1569    $transit->dest_recv_time('now');
1570         $self->bail_on_events($self->editor->event)
1571                 unless $self->editor->update_action_transit_copy($transit);
1572
1573         my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1574
1575    $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1576    $copy->status( $transit->copy_status );
1577         $self->update_copy();
1578         return if $self->bail_out;
1579
1580         my $ishold = 0;
1581         if($hold_transit) {     
1582                 $self->do_hold_notify($hold_transit->hold);
1583                 $ishold = 1;
1584         }
1585
1586         $self->push_events( 
1587                 OpenILS::Event->new(
1588                 'SUCCESS', 
1589                 ishold => $ishold,
1590       payload => { transit => $transit, holdtransit => $hold_transit } ));
1591
1592         return $hold_transit;
1593 }
1594
1595
1596 sub checkin_handle_circ {
1597    my $self = shift;
1598         $U->logmark;
1599
1600    my $circ = $self->circ;
1601    my $copy = $self->copy;
1602    my $evt;
1603    my $obt;
1604
1605    # backdate the circ if necessary
1606    if($self->backdate) {
1607                 $self->checkin_handle_backdate;
1608                 return if $self->bail_out;
1609    }
1610
1611    if(!$circ->stop_fines) {
1612       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1613       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1614       $circ->stop_fines_time('now') unless $self->backdate;
1615       $circ->stop_fines_time($self->backdate) if $self->backdate;
1616    }
1617
1618    # see if there are any fines owed on this circ.  if not, close it
1619         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1620    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1621
1622    # Set the checkin vars since we have the item
1623    $circ->checkin_time('now');
1624    $circ->checkin_staff($self->editor->requestor->id);
1625    $circ->checkin_lib($self->editor->requestor->ws_ou);
1626
1627         my $circ_lib = (ref $self->copy->circ_lib) ?  
1628                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1629         my $stat = $U->copy_status($self->copy->status)->id;
1630
1631         # If the item is lost/missing and it needs to be sent home, don't 
1632         # reshelve the copy, leave it lost/missing so the recipient will know
1633         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1634                 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1635                 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1636
1637         } else {
1638                 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1639                 $self->update_copy;
1640         }
1641
1642
1643         return $self->bail_on_events($self->editor->event)
1644                 unless $self->editor->update_action_circulation($circ);
1645 }
1646
1647
1648 sub checkin_handle_backdate {
1649         my $self = shift;
1650
1651         my $bd = $self->backdate;
1652         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1653         $bd = "${bd}T23:59:59";
1654
1655         my $bills = $self->editor->search_money_billing(
1656                 { 
1657                         billing_ts => { '>=' => $bd }, 
1658                         xact => $self->circ->id, 
1659                         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1660                 }
1661         );
1662
1663         for my $bill (@$bills) {        
1664                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1665                         $bill->voided('t');
1666                         my $n = $bill->note || "";
1667                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1668
1669                         $self->bail_on_events($self->editor->event)
1670                                 unless $self->editor->update_money_billing($bill);
1671                 }
1672         }
1673 }
1674
1675
1676
1677 # XXX Legacy version for Circ.pm support
1678 sub _checkin_handle_backdate {
1679    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1680
1681         my $bd = $backdate;
1682         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1683         $bd = "${bd}T23:59:59";
1684
1685
1686    my $bills = $session->request(
1687       "open-ils.storage.direct.money.billing.search_where.atomic",
1688                 billing_ts => { '>=' => $bd }, 
1689                 xact => $circ->id,
1690                 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1691         )->gather(1);
1692
1693    if($bills) {
1694       for my $bill (@$bills) {
1695          $bill->voided('t');
1696          my $n = $bill->note || "";
1697          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1698          my $s = $session->request(
1699             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1700          return $U->DB_UPDATE_FAILED($bill) unless $s;
1701       }
1702    }
1703 }
1704
1705
1706
1707
1708
1709
1710 sub find_patron_from_copy {
1711         my $self = shift;
1712         my $circs = $self->editor->search_action_circulation(
1713                 { target_copy => $self->copy->id, checkin_time => undef });
1714         my $circ = $circs->[0];
1715         return unless $circ;
1716         my $u = $self->editor->retrieve_actor_user($circ->usr)
1717                 or return $self->bail_on_events($self->editor->event);
1718         $self->patron($u);
1719 }
1720
1721 sub check_checkin_copy_status {
1722         my $self = shift;
1723    my $copy = $self->copy;
1724
1725    my $islost     = 0;
1726    my $ismissing  = 0;
1727    my $evt        = undef;
1728
1729    my $status = $U->copy_status($copy->status)->id;
1730
1731    return undef
1732       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1733             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1734             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1735             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1736             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1737             $status == OILS_COPY_STATUS_CATALOGING  ||
1738             $status == OILS_COPY_STATUS_RESHELVING );
1739
1740    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1741       if( $status == OILS_COPY_STATUS_LOST );
1742
1743    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1744       if( $status == OILS_COPY_STATUS_MISSING );
1745
1746    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1747 }
1748
1749
1750
1751 # --------------------------------------------------------------------------
1752 # On checkin, we need to return as many relevant objects as we can
1753 # --------------------------------------------------------------------------
1754 sub checkin_flesh_events {
1755         my $self = shift;
1756
1757         if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
1758                 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1759                         $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1760         }
1761
1762
1763         for my $evt (@{$self->events}) {
1764
1765                 my $payload          = {};
1766                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1767                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1768                 $payload->{circ}     = $self->circ;
1769                 $payload->{transit}  = $self->transit;
1770                 $payload->{hold}     = $self->hold;
1771                 
1772                 $evt->{payload} = $payload;
1773         }
1774 }
1775
1776 sub log_me {
1777         my( $self, $msg ) = @_;
1778         my $bc = ($self->copy) ? $self->copy->barcode :
1779                 $self->barcode;
1780         $bc ||= "";
1781         my $usr = ($self->patron) ? $self->patron->id : "";
1782         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1783                 ", recipient=$usr, copy=$bc");
1784 }
1785
1786
1787 sub do_renew {
1788         my $self = shift;
1789         $self->log_me("do_renew()");
1790         $self->is_renewal(1);
1791
1792         unless( $self->is_renewal ) {
1793                 return $self->bail_on_events($self->editor->events)
1794                         unless $self->editor->allowed('RENEW_CIRC');
1795         }       
1796
1797         # Make sure there is an open circ to renew that is not
1798         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1799         my $circ = $self->editor->search_action_circulation(
1800                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1801
1802         if(!$circ) {
1803                 $circ = $self->editor->search_action_circulation(
1804                         { 
1805                                 target_copy => $self->copy->id, 
1806                                 stop_fines => OILS_STOP_FINES_MAX_FINES,
1807                                 checkin_time => undef
1808                         } 
1809                 )->[0];
1810         }
1811
1812         return $self->bail_on_events($self->editor->event) unless $circ;
1813
1814         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1815                 if $circ->renewal_remaining < 1;
1816
1817         # -----------------------------------------------------------------
1818
1819         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1820         $self->circ($circ);
1821
1822         $self->run_renew_permit;
1823
1824         # Check the item in
1825         $self->do_checkin();
1826         return if $self->bail_out;
1827
1828         unless( $self->permit_override ) {
1829                 $self->do_permit();
1830                 return if $self->bail_out;
1831                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1832                 $self->remove_event('ITEM_NOT_CATALOGED');
1833         }       
1834
1835         $self->override_events;
1836         return if $self->bail_out;
1837
1838         $self->events([]);
1839         $self->do_checkout();
1840 }
1841
1842
1843 sub remove_event {
1844         my( $self, $evt ) = @_;
1845         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1846         $logger->debug("circulator: removing event from list: $evt");
1847         my @events = @{$self->events};
1848         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1849 }
1850
1851
1852 sub have_event {
1853         my( $self, $evt ) = @_;
1854         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1855         return grep { $_->{textcode} eq $evt } @{$self->events};
1856 }
1857
1858
1859
1860 sub run_renew_permit {
1861         my $self = shift;
1862    my $runner = $self->script_runner;
1863
1864    $runner->load($self->circ_permit_renew);
1865    my $result = $runner->run or 
1866                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1867    my $events = $result->{events};
1868
1869    $logger->activity("ciculator: circ_permit_renew for user ".
1870       $self->patron->id." returned events: @$events") if @$events;
1871
1872         $self->push_events(OpenILS::Event->new($_)) for @$events;
1873         
1874         $logger->debug("circulator: re-creating script runner to be safe");
1875         $self->mk_script_runner;
1876 }
1877
1878
1879
1880