]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
changed hold creation to make request_lib the ws_ou of the requestor - also added...
[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 undef if $self->is_renewal;
694         return OpenILS::Event->new(
695                 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
696                 if $self->copy and $self->copy->alert_message;
697         return undef;
698 }
699
700
701
702 # --------------------------------------------------------------------------
703 # If the call is overriding and has permissions to override every collected
704 # event, the are cleared.  Any event that the caller does not have
705 # permission to override, will be left in the event list and bail_out will
706 # be set
707 # XXX We need code in here to cancel any holds/transits on copies 
708 # that are being force-checked out
709 # --------------------------------------------------------------------------
710 sub override_events {
711         my $self = shift;
712         my @events = @{$self->events};
713         return unless @events;
714
715         if(!$self->override) {
716                 return $self->bail_out(1) 
717                         if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
718         }       
719
720         $self->events([]);
721         
722    for my $e (@events) {
723       my $tc = $e->{textcode};
724       next if $tc eq 'SUCCESS';
725       my $ov = "$tc.override";
726       $logger->info("circulator: attempting to override event: $ov");
727
728                 return $self->bail_on_events($self->editor->event)
729                         unless( $self->editor->allowed($ov)     );
730    }
731 }
732         
733
734 # --------------------------------------------------------------------------
735 # If there is an open claimsreturn circ on the requested copy, close the 
736 # circ if overriding, otherwise bail out
737 # --------------------------------------------------------------------------
738 sub handle_claims_returned {
739         my $self = shift;
740         my $copy = $self->copy;
741
742         my $CR = $self->editor->search_action_circulation(
743                 {       
744                         target_copy             => $copy->id,
745                         stop_fines              => OILS_STOP_FINES_CLAIMSRETURNED,
746                         checkin_time    => undef,
747                 }
748         );
749
750         return unless ($CR = $CR->[0]); 
751
752         my $evt;
753
754         # - If the caller has set the override flag, we will check the item in
755         if($self->override) {
756
757                 $CR->checkin_time('now');       
758                 $CR->checkin_lib($self->editor->requestor->ws_ou);
759                 $CR->checkin_staff($self->editor->requestor->id);
760
761                 $evt = $self->editor->event 
762                         unless $self->editor->update_action_circulation($CR);
763
764         } else {
765                 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
766         }
767
768         $self->bail_on_events($evt) if $evt;
769         return;
770 }
771
772
773 # --------------------------------------------------------------------------
774 # This performs the checkout
775 # --------------------------------------------------------------------------
776 sub do_checkout {
777         my $self = shift;
778
779         $self->log_me("do_checkout()");
780
781         # make sure perms are good if this isn't a renewal
782         unless( $self->is_renewal ) {
783                 return $self->bail_on_events($self->editor->event)
784                         unless( $self->editor->allowed('COPY_CHECKOUT') );
785         }
786
787         # verify the permit key
788         unless( $self->check_permit_key ) {
789                 if( $self->permit_override ) {
790                         return $self->bail_on_events($self->editor->event)
791                                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
792                 } else {
793                         return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
794                 }       
795         }
796
797         # if this is a non-cataloged circ, build the circ and finish
798         if( $self->is_noncat ) {
799                 $self->checkout_noncat;
800                 $self->push_events(
801                         OpenILS::Event->new('SUCCESS', 
802                         payload => { noncat_circ => $self->circ }));
803                 return;
804         }
805
806         if( $self->is_precat ) {
807                 $self->script_runner->insert("environment.isPrecat", 1, 1);
808                 $self->make_precat_copy;
809                 return if $self->bail_out;
810
811         } elsif( $self->copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
812                 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
813         }
814
815         $self->do_copy_checks;
816         return if $self->bail_out;
817
818         $self->run_checkout_scripts();
819         return if $self->bail_out;
820
821         $self->build_checkout_circ_object();
822         return if $self->bail_out;
823
824         $self->apply_modified_due_date();
825         return if $self->bail_out;
826
827         return $self->bail_on_events($self->editor->event)
828                 unless $self->editor->create_action_circulation($self->circ);
829
830         $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
831         $self->update_copy;
832         return if $self->bail_out;
833
834         $self->handle_checkout_holds();
835         return if $self->bail_out;
836
837    # ------------------------------------------------------------------------------
838    # Update the patron penalty info in the DB
839    # ------------------------------------------------------------------------------
840    $U->update_patron_penalties(
841       authtoken => $self->editor->authtoken,
842       patron    => $self->patron,
843       background  => 1,
844    );
845
846         my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
847         $self->push_events(
848                 OpenILS::Event->new('SUCCESS',
849                         payload  => {
850                                 copy              => $U->unflesh_copy($self->copy),
851                                 circ              => $self->circ,
852                                 record            => $record,
853                                 holds_fulfilled   => $self->fulfilled_holds,
854                         }
855                 )
856         );
857 }
858
859 sub update_copy {
860         my $self = shift;
861         my $copy = $self->copy;
862
863         my $stat = $copy->status if ref $copy->status;
864         my $loc = $copy->location if ref $copy->location;
865         my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
866
867         $copy->status($stat->id) if $stat;
868         $copy->location($loc->id) if $loc;
869         $copy->circ_lib($circ_lib->id) if $circ_lib;
870         $copy->editor($self->editor->requestor->id);
871         $copy->edit_date('now');
872         $copy->age_protect($copy->age_protect->id) if ref $copy->age_protect;
873
874         return $self->bail_on_events($self->editor->event)
875                 unless $self->editor->update_asset_copy($self->copy);
876
877         $copy->status($U->copy_status($copy->status));
878         $copy->location($loc) if $loc;
879         $copy->circ_lib($circ_lib) if $circ_lib;
880 }
881
882
883 sub bail_on_events {
884         my( $self, @evts ) = @_;
885         $self->push_events(@evts);
886         $self->bail_out(1);
887 }
888
889 sub handle_checkout_holds {
890    my $self    = shift;
891
892    my $copy    = $self->copy;
893    my $patron  = $self->patron;
894
895         my $holds       = $self->editor->search_action_hold_request(
896                 { 
897                         current_copy            => $copy->id , 
898                         cancel_time                     => undef, 
899                         fulfillment_time        => undef 
900                 }
901         );
902
903    my @fulfilled;
904
905    # XXX We should only fulfill one hold here...
906    # XXX If a hold was transited to the user who is checking out
907    # the item, we need to make sure that hold is what's grabbed
908    if(@$holds) {
909
910       # for now, just sort by id to get what should be the oldest hold
911       $holds = [ sort { $a->id <=> $b->id } @$holds ];
912       my @myholds = grep { $_->usr eq $patron->id } @$holds;
913       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
914
915       if(@myholds) {
916          my $hold = $myholds[0];
917
918          $logger->debug("circulator: related hold found in checkout: " . $hold->id );
919
920          # if the hold was never officially captured, capture it.
921          $hold->capture_time('now') unless $hold->capture_time;
922
923                         # just make sure it's set correctly
924          $hold->current_copy($copy->id); 
925
926          $hold->fulfillment_time('now');
927                         $hold->fulfillment_staff($self->editor->requestor->id);
928                         $hold->fulfillment_lib($self->editor->requestor->ws_ou);
929
930                         return $self->bail_on_events($self->editor->event)
931                                 unless $self->editor->update_action_hold_request($hold);
932
933                         $holdcode->delete_hold_copy_maps($self->editor, $hold->id);
934
935          push( @fulfilled, $hold->id );
936       }
937
938       # If there are any holds placed for other users that point to this copy,
939       # then we need to un-target those holds so the targeter can pick a new copy
940       for(@altholds) {
941
942          $logger->info("circulator: un-targeting hold ".$_->id.
943             " because copy ".$copy->id." is getting checked out");
944
945                         # - make the targeter process this hold at next run
946          $_->clear_prev_check_time; 
947
948                         # - clear out the targetted copy
949          $_->clear_current_copy;
950          $_->clear_capture_time;
951
952                         return $self->bail_on_event($self->editor->event)
953                                 unless $self->editor->update_action_hold_request($_);
954       }
955    }
956
957         $self->fulfilled_holds(\@fulfilled);
958 }
959
960
961
962 sub run_checkout_scripts {
963         my $self = shift;
964
965         my $evt;
966    my $runner = $self->script_runner;
967    $runner->load($self->circ_duration);
968
969    my $result = $runner->run or 
970                 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
971
972    my $duration   = $result->{durationRule};
973    my $recurring  = $result->{recurringFinesRule};
974    my $max_fine   = $result->{maxFine};
975
976         if( $duration ne OILS_UNLIMITED_CIRC_DURATION ) {
977
978                 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
979                 return $self->bail_on_events($evt) if $evt;
980         
981                 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
982                 return $self->bail_on_events($evt) if $evt;
983         
984                 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
985                 return $self->bail_on_events($evt) if $evt;
986
987         } else {
988
989                 # The item circulates with an unlimited duration
990                 $duration       = undef;
991                 $recurring      = undef;
992                 $max_fine       = undef;
993         }
994
995    $self->duration_rule($duration);
996    $self->recurring_fines_rule($recurring);
997    $self->max_fine_rule($max_fine);
998 }
999
1000
1001 sub build_checkout_circ_object {
1002         my $self = shift;
1003
1004    my $circ       = Fieldmapper::action::circulation->new;
1005    my $duration   = $self->duration_rule;
1006    my $max        = $self->max_fine_rule;
1007    my $recurring  = $self->recurring_fines_rule;
1008    my $copy       = $self->copy;
1009    my $patron     = $self->patron;
1010
1011         if( $duration ) {
1012
1013                 my $dname = $duration->name;
1014                 my $mname = $max->name;
1015                 my $rname = $recurring->name;
1016         
1017                 $logger->debug("circulator: building circulation ".
1018                         "with duration=$dname, maxfine=$mname, recurring=$rname");
1019         
1020                 $circ->duration( $duration->shrt ) 
1021                         if $copy->loan_duration == OILS_CIRC_DURATION_SHORT;
1022                 $circ->duration( $duration->normal ) 
1023                         if $copy->loan_duration == OILS_CIRC_DURATION_NORMAL;
1024                 $circ->duration( $duration->extended ) 
1025                         if $copy->loan_duration == OILS_CIRC_DURATION_EXTENDED;
1026         
1027                 $circ->recuring_fine( $recurring->low ) 
1028                         if $copy->fine_level == OILS_REC_FINE_LEVEL_LOW;
1029                 $circ->recuring_fine( $recurring->normal ) 
1030                         if $copy->fine_level == OILS_REC_FINE_LEVEL_NORMAL;
1031                 $circ->recuring_fine( $recurring->high ) 
1032                         if $copy->fine_level == OILS_REC_FINE_LEVEL_HIGH;
1033
1034                 $circ->duration_rule( $duration->name );
1035                 $circ->recuring_fine_rule( $recurring->name );
1036                 $circ->max_fine_rule( $max->name );
1037                 $circ->max_fine( $max->amount );
1038
1039                 $circ->fine_interval($recurring->recurance_interval);
1040                 $circ->renewal_remaining( $duration->max_renewals );
1041
1042         } else {
1043
1044                 $logger->info("circulator: copy found with an unlimited circ duration");
1045                 $circ->duration_rule(OILS_UNLIMITED_CIRC_DURATION);
1046                 $circ->recuring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1047                 $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
1048                 $circ->renewal_remaining(0);
1049         }
1050
1051    $circ->target_copy( $copy->id );
1052    $circ->usr( $patron->id );
1053    $circ->circ_lib( $self->circ_lib );
1054
1055    if( $self->is_renewal ) {
1056       $circ->opac_renewal(1);
1057       $circ->renewal_remaining($self->renewal_remaining);
1058       $circ->circ_staff($self->editor->requestor->id);
1059    }
1060
1061    # if the user provided an overiding checkout time,
1062    # (e.g. the checkout really happened several hours ago), then
1063    # we apply that here.  Does this need a perm??
1064         $circ->xact_start(clense_ISO8601($self->checkout_time))
1065                 if $self->checkout_time;
1066
1067    # if a patron is renewing, 'requestor' will be the patron
1068    $circ->circ_staff($self->editor->requestor->id);
1069         $circ->due_date( $self->create_due_date($circ->duration) ) if $circ->duration;
1070
1071         $self->circ($circ);
1072 }
1073
1074
1075 sub apply_modified_due_date {
1076         my $self = shift;
1077         my $circ = $self->circ;
1078         my $copy = $self->copy;
1079
1080    if( $self->due_date ) {
1081
1082                 return $self->bail_on_events($self->editor->event)
1083                         unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
1084
1085       $circ->due_date(clense_ISO8601($self->due_date));
1086
1087    } else {
1088
1089       # if the due_date lands on a day when the location is closed
1090       return unless $copy and $circ->due_date;
1091
1092                 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
1093
1094       $logger->info("circulator: circ searching for closed date overlap on lib $org".
1095                         " with an item due date of ".$circ->due_date );
1096
1097       my $dateinfo = $U->storagereq(
1098          'open-ils.storage.actor.org_unit.closed_date.overlap', 
1099                         $org, $circ->due_date );
1100
1101       if($dateinfo) {
1102          $logger->info("circulator: $dateinfo : circ due data / close date overlap found : due_date=".
1103             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1104
1105             # XXX make the behavior more dynamic
1106             # for now, we just push the due date to after the close date
1107             $circ->due_date($dateinfo->{end});
1108       }
1109    }
1110 }
1111
1112
1113
1114 sub create_due_date {
1115         my( $self, $duration ) = @_;
1116    my ($sec,$min,$hour,$mday,$mon,$year) =
1117       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1118    $year += 1900; $mon += 1;
1119    my $due_date = sprintf(
1120       '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1121       $year, $mon, $mday, $hour, $min, $sec);
1122    return $due_date;
1123 }
1124
1125
1126
1127 sub make_precat_copy {
1128         my $self = shift;
1129         my $copy = $self->copy;
1130
1131    if($copy) {
1132       $logger->debug("ciculator: Pre-cat copy already exists in checkout: ID=" . $copy->id);
1133
1134       $copy->editor($self->editor->requestor->id);
1135       $copy->edit_date('now');
1136       $copy->dummy_title($self->dummy_title);
1137       $copy->dummy_author($self->dummy_author);
1138
1139                 $self->update_copy();
1140                 return;
1141    }
1142
1143    $logger->info("circulator: Creating a new precataloged ".
1144                 "copy in checkout with barcode " . $self->copy_barcode);
1145
1146    $copy = Fieldmapper::asset::copy->new;
1147    $copy->circ_lib($self->circ_lib);
1148    $copy->creator($self->editor->requestor->id);
1149    $copy->editor($self->editor->requestor->id);
1150    $copy->barcode($self->copy_barcode);
1151    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
1152    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
1153    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
1154
1155    $copy->dummy_title($self->dummy_title || "");
1156    $copy->dummy_author($self->dummy_author || "");
1157
1158         unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1159                 $self->bail_out(1);
1160                 $self->push_events($self->editor->event);
1161                 return;
1162         }       
1163
1164         # this is a little bit of a hack, but we need to 
1165         # get the copy into the script runner
1166         $self->script_runner->insert("environment.copy", $copy, 1);
1167 }
1168
1169
1170 sub checkout_noncat {
1171         my $self = shift;
1172
1173         my $circ;
1174         my $evt;
1175
1176    my $lib              = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1177    my $count    = $self->noncat_count || 1;
1178    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1179
1180    $logger->info("ciculator: circ creating $count noncat circs with checkout time $cotime");
1181
1182    for(1..$count) {
1183
1184       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1185          $self->editor->requestor->id, 
1186                         $self->patron->id, 
1187                         $lib, 
1188                         $self->noncat_type, 
1189                         $cotime,
1190                         $self->editor );
1191
1192                 if( $evt ) {
1193                         $self->push_events($evt);
1194                         $self->bail_out(1);
1195                         return; 
1196                 }
1197                 $self->circ($circ);
1198    }
1199 }
1200
1201
1202 sub do_checkin {
1203         my $self = shift;
1204         $self->log_me("do_checkin()");
1205
1206
1207         return $self->bail_on_events(
1208                 OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) 
1209                 unless $self->copy;
1210
1211         if( $self->checkin_check_holds_shelf() ) {
1212                 $self->bail_on_events(OpenILS::Event->new('NO_CHANGE'));
1213                 $self->hold($U->fetch_open_hold_by_copy($self->copy->id));
1214                 $self->checkin_flesh_events;
1215                 return;
1216         }
1217
1218         unless( $self->is_renewal ) {
1219                 return $self->bail_on_events($self->editor->event)
1220                         unless $self->editor->allowed('COPY_CHECKIN');
1221         }
1222
1223         $self->push_events($self->check_copy_alert());
1224         $self->push_events($self->check_checkin_copy_status());
1225
1226         # the renew code will have already found our circulation object
1227         unless( $self->is_renewal and $self->circ ) {
1228                 $self->circ(
1229                         $self->editor->search_action_circulation(
1230                         { target_copy => $self->copy->id, checkin_time => undef })->[0]);
1231         }
1232
1233         # if the circ is marked as 'claims returned', add the event to the list
1234         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1235                 if ($self->circ and $self->circ->stop_fines 
1236                                 and $self->circ->stop_fines eq OILS_STOP_FINES_CLAIMSRETURNED);
1237
1238         # handle the overridable events 
1239         $self->override_events unless $self->is_renewal;
1240         return if $self->bail_out;
1241         
1242         if( $self->copy ) {
1243                 $self->transit(
1244                         $self->editor->search_action_transit_copy(
1245                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1246         }
1247
1248         if( $self->circ ) {
1249                 $self->checkin_handle_circ;
1250                 return if $self->bail_out;
1251                 $self->checkin_changed(1);
1252
1253         } elsif( $self->transit ) {
1254                 my $hold_transit = $self->process_received_transit;
1255                 $self->checkin_changed(1);
1256
1257                 if( $self->bail_out ) { 
1258                         $self->checkin_flesh_events;
1259                         return;
1260                 }
1261                 
1262                 if( my $e = $self->check_checkin_copy_status() ) {
1263                         # If the original copy status is special, alert the caller
1264                         my $ev = $self->events;
1265                         $self->events([$e]);
1266                         $self->override_events;
1267                         return if $self->bail_out;
1268                         $self->events($ev);
1269                 }
1270
1271                 if( $hold_transit or 
1272                                 $U->copy_status($self->copy->status)->id 
1273                                         == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1274                         $self->hold(
1275                                 ($hold_transit) ?
1276                                         $self->editor->retrieve_action_hold_request($hold_transit->hold) :
1277                                         $U->fetch_open_hold_by_copy($self->copy->id)
1278                                 );
1279
1280                         $self->checkin_flesh_events;
1281                         return;
1282                 } 
1283         }
1284
1285         if( $self->is_renewal ) {
1286                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1287                 return;
1288         }
1289
1290    # ------------------------------------------------------------------------------
1291    # Circulations and transits are now closed where necessary.  Now go on to see if
1292    # this copy can fulfill a hold or needs to be routed to a different location
1293    # ------------------------------------------------------------------------------
1294
1295         if( !$self->remote_hold and $self->attempt_checkin_hold_capture() ) {
1296                 return if $self->bail_out;
1297
1298    } else { # not needed for a hold
1299
1300
1301                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1302                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1303
1304                 if( $self->remote_hold ) {
1305                         $circ_lib = $self->remote_hold->pickup_lib;
1306                         $logger->warn("circulator: Copy ".$self->copy->barcode.
1307                                 " is on a remote hold's shelf, sending to $circ_lib");
1308                 }
1309
1310                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1311
1312       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1313
1314                         $self->checkin_handle_precat();
1315                         return if $self->bail_out;
1316
1317       } else {
1318
1319                         my $bc = $self->copy->barcode;
1320                         $logger->info("circulator: copy $bc at a remote lib  - sending home");
1321                         $self->checkin_build_copy_transit();
1322                         return if $self->bail_out;
1323                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1324       }
1325    }
1326
1327         $self->reshelve_copy;
1328         return if $self->bail_out;
1329
1330         unless($self->checkin_changed) {
1331
1332                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1333                 my $stat = $U->copy_status($self->copy->status)->id;
1334
1335         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1336          if( $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF );
1337                 $self->bail_out(1); # no need to commit anything
1338
1339         } else {
1340                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1341                         unless @{$self->events};
1342         }
1343
1344         $self->checkin_flesh_events;
1345         return;
1346 }
1347
1348 sub reshelve_copy {
1349    my $self    = shift;
1350    my $force   = $self->force || shift;
1351    my $copy    = $self->copy;
1352
1353    my $stat = $U->copy_status($copy->status)->id;
1354
1355    if($force || (
1356       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
1357       $stat != OILS_COPY_STATUS_CATALOGING and
1358       $stat != OILS_COPY_STATUS_IN_TRANSIT and
1359       $stat != OILS_COPY_STATUS_RESHELVING  )) {
1360
1361         $copy->status( OILS_COPY_STATUS_RESHELVING );
1362                         $self->update_copy;
1363                         $self->checkin_changed(1);
1364         }
1365 }
1366
1367
1368 # Returns true if the item is at the current location
1369 # because it was transited there for a hold and the 
1370 # hold has not been fulfilled
1371 sub checkin_check_holds_shelf {
1372         my $self = shift;
1373         return 0 unless $self->copy;
1374
1375         return 0 unless 
1376                 $U->copy_status($self->copy->status)->id ==
1377                         OILS_COPY_STATUS_ON_HOLDS_SHELF;
1378
1379         # find the hold that put us on the holds shelf
1380         my $holds = $self->editor->search_action_hold_request(
1381                 { 
1382                         current_copy => $self->copy->id,
1383                         capture_time => { '!=' => undef },
1384                         fulfillment_time => undef,
1385                         cancel_time => undef,
1386                 }
1387         );
1388
1389         return 0 unless @$holds;
1390
1391         my $hold = $$holds[0];
1392
1393         $logger->info("circulator: we found a captured, un-fulfilled hold [".
1394                 $hold->id. "] for copy ".$self->copy->barcode);
1395
1396         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1397                 $logger->info("circulator: hold is for here .. we're done: ".$self->copy->barcode);
1398                 return 1;
1399         }
1400
1401         $logger->info("circulator: hold is not for here..");
1402         $self->remote_hold($hold);
1403         return 0;
1404 }
1405
1406
1407 sub checkin_handle_precat {
1408         my $self        = shift;
1409    my $copy    = $self->copy;
1410
1411    if( $self->is_precat and ($copy->status != OILS_COPY_STATUS_CATALOGING) ) {
1412       $copy->status(OILS_COPY_STATUS_CATALOGING);
1413                 $self->update_copy();
1414                 $self->checkin_changed(1);
1415                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1416    }
1417 }
1418
1419
1420 sub checkin_build_copy_transit {
1421         my $self                        = shift;
1422         my $copy       = $self->copy;
1423    my $transit    = Fieldmapper::action::transit_copy->new;
1424
1425    $transit->source($self->editor->requestor->ws_ou);
1426    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1427    $transit->target_copy($copy->id);
1428    $transit->source_send_time('now');
1429    $transit->copy_status( $U->copy_status($copy->status)->id );
1430
1431         $logger->debug("circulator: setting copy status on transit: ".$transit->copy_status);
1432
1433         return $self->bail_on_events($self->editor->event)
1434                 unless $self->editor->create_action_transit_copy($transit);
1435
1436    $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1437         $self->update_copy;
1438         $self->checkin_changed(1);
1439 }
1440
1441
1442 sub attempt_checkin_hold_capture {
1443         my $self = shift;
1444         my $copy = $self->copy;
1445
1446         # See if this copy can fulfill any holds
1447         my ($hold) = $holdcode->find_nearest_permitted_hold(
1448                 OpenSRF::AppSession->create('open-ils.storage'), 
1449                 $copy, $self->editor->requestor );
1450
1451         if(!$hold) {
1452                 $logger->debug("circulator: no potential permitted".
1453                         "holds found for copy ".$copy->barcode);
1454                 return undef;
1455         }
1456
1457
1458         $logger->info("circulator: found permitted hold ".
1459                 $hold->id . " for copy, capturing...");
1460
1461         $hold->current_copy($copy->id);
1462         $hold->capture_time('now');
1463
1464         # prevent DB errors caused by fetching 
1465         # holds from storage, and updating through cstore
1466         $hold->clear_fulfillment_time;
1467         $hold->clear_fulfillment_staff;
1468         $hold->clear_fulfillment_lib;
1469         $hold->clear_expire_time; 
1470         $hold->clear_cancel_time;
1471         $hold->clear_prev_check_time unless $hold->prev_check_time;
1472
1473         $self->bail_on_events($self->editor->event)
1474                 unless $self->editor->update_action_hold_request($hold);
1475         $self->hold($hold);
1476         $self->checkin_changed(1);
1477
1478         return 1 if $self->bail_out;
1479
1480         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1481
1482                 # This hold was captured in the correct location
1483         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1484                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1485
1486                 $self->do_hold_notify($hold->id);
1487
1488         } else {
1489         
1490                 # Hold needs to be picked up elsewhere.  Build a hold
1491                 # transit and route the item.
1492                 $self->checkin_build_hold_transit();
1493         $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1494                 return 1 if $self->bail_out;
1495                 $self->push_events(
1496                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1497         }
1498
1499         # make sure we save the copy status
1500         $self->update_copy;
1501         return 1;
1502 }
1503
1504 sub do_hold_notify {
1505         my( $self, $holdid ) = @_;
1506         my $notifier = OpenILS::Application::Circ::HoldNotify->new(
1507                 editor => $self->editor, hold_id => $holdid );
1508
1509         if(!$notifier->event) {
1510
1511                 $logger->info("ciculator: attempt at sending hold notification for hold $holdid");
1512
1513                 my $stat = $notifier->send_email_notify;
1514                 $logger->info("ciculator: hold notify succeeded for hold $holdid") if $stat eq '1';
1515                 $logger->warn("ciculator:  * hold notify failed for hold $holdid") if $stat ne '1';
1516
1517         } else {
1518                 $logger->info("ciculator: Not sending hold notification since the patron has no email address");
1519         }
1520 }
1521
1522
1523 sub checkin_build_hold_transit {
1524         my $self = shift;
1525
1526
1527    my $copy = $self->copy;
1528    my $hold = $self->hold;
1529    my $trans = Fieldmapper::action::hold_transit_copy->new;
1530
1531         $logger->debug("circulator: building hold transit for ".$copy->barcode);
1532
1533    $trans->hold($hold->id);
1534    $trans->source($self->editor->requestor->ws_ou);
1535    $trans->dest($hold->pickup_lib);
1536    $trans->source_send_time("now");
1537    $trans->target_copy($copy->id);
1538
1539         # when the copy gets to its destination, it will recover
1540         # this status - put it onto the holds shelf
1541    $trans->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1542
1543         return $self->bail_on_events($self->editor->event)
1544                 unless $self->editor->create_action_hold_transit_copy($trans);
1545 }
1546
1547
1548
1549 sub process_received_transit {
1550         my $self = shift;
1551         my $copy = $self->copy;
1552    my $copyid = $self->copy->id;
1553
1554         my $status_name = $U->copy_status($copy->status)->name;
1555    $logger->debug("circulator: attempting transit receive on ".
1556                 "copy $copyid. Copy status is $status_name");
1557
1558         my $transit = $self->transit;
1559
1560    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1561       $logger->info("circulator: Fowarding transit on copy which is destined ".
1562          "for a different location. copy=$copyid,current ".
1563          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1564
1565                 return $self->bail_on_events(
1566                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1567    }
1568
1569    # The transit is received, set the receive time
1570    $transit->dest_recv_time('now');
1571         $self->bail_on_events($self->editor->event)
1572                 unless $self->editor->update_action_transit_copy($transit);
1573
1574         my $hold_transit = $self->editor->retrieve_action_hold_transit_copy($transit->id);
1575
1576    $logger->info("ciculator: Recovering original copy status in transit: ".$transit->copy_status);
1577    $copy->status( $transit->copy_status );
1578         $self->update_copy();
1579         return if $self->bail_out;
1580
1581         my $ishold = 0;
1582         if($hold_transit) {     
1583                 $self->do_hold_notify($hold_transit->hold);
1584                 $ishold = 1;
1585         }
1586
1587         $self->push_events( 
1588                 OpenILS::Event->new(
1589                 'SUCCESS', 
1590                 ishold => $ishold,
1591       payload => { transit => $transit, holdtransit => $hold_transit } ));
1592
1593         return $hold_transit;
1594 }
1595
1596
1597 sub checkin_handle_circ {
1598    my $self = shift;
1599         $U->logmark;
1600
1601    my $circ = $self->circ;
1602    my $copy = $self->copy;
1603    my $evt;
1604    my $obt;
1605
1606    # backdate the circ if necessary
1607    if($self->backdate) {
1608                 $self->checkin_handle_backdate;
1609                 return if $self->bail_out;
1610    }
1611
1612    if(!$circ->stop_fines) {
1613       $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
1614       $circ->stop_fines(OILS_STOP_FINES_RENEW) if $self->is_renewal;
1615       $circ->stop_fines_time('now') unless $self->backdate;
1616       $circ->stop_fines_time($self->backdate) if $self->backdate;
1617    }
1618
1619    # see if there are any fines owed on this circ.  if not, close it
1620         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1621    $circ->xact_finish('now') if( $obt and $obt->balance_owed == 0 );
1622
1623    # Set the checkin vars since we have the item
1624    $circ->checkin_time('now');
1625    $circ->checkin_staff($self->editor->requestor->id);
1626    $circ->checkin_lib($self->editor->requestor->ws_ou);
1627
1628         my $circ_lib = (ref $self->copy->circ_lib) ?  
1629                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1630         my $stat = $U->copy_status($self->copy->status)->id;
1631
1632         # If the item is lost/missing and it needs to be sent home, don't 
1633         # reshelve the copy, leave it lost/missing so the recipient will know
1634         if( ($stat == OILS_COPY_STATUS_LOST or $stat == OILS_COPY_STATUS_MISSING)
1635                 and ($circ_lib != $self->editor->requestor->ws_ou) ) {
1636                 $logger->info("circulator: not updating copy status on checkin because copy is lost/missing");
1637
1638         } else {
1639                 $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
1640                 $self->update_copy;
1641         }
1642
1643
1644         return $self->bail_on_events($self->editor->event)
1645                 unless $self->editor->update_action_circulation($circ);
1646 }
1647
1648
1649 sub checkin_handle_backdate {
1650         my $self = shift;
1651
1652         my $bd = $self->backdate;
1653         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1654         $bd = "${bd}T23:59:59";
1655
1656         my $bills = $self->editor->search_money_billing(
1657                 { 
1658                         billing_ts => { '>=' => $bd }, 
1659                         xact => $self->circ->id, 
1660                         billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1661                 }
1662         );
1663
1664         for my $bill (@$bills) {        
1665                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1666                         $bill->voided('t');
1667                         my $n = $bill->note || "";
1668                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1669
1670                         $self->bail_on_events($self->editor->event)
1671                                 unless $self->editor->update_money_billing($bill);
1672                 }
1673         }
1674 }
1675
1676
1677
1678 # XXX Legacy version for Circ.pm support
1679 sub _checkin_handle_backdate {
1680    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1681
1682         my $bd = $backdate;
1683         $bd =~ s/^(\d{4}-\d{2}-\d{2}).*/$1/og;
1684         $bd = "${bd}T23:59:59";
1685
1686
1687    my $bills = $session->request(
1688       "open-ils.storage.direct.money.billing.search_where.atomic",
1689                 billing_ts => { '>=' => $bd }, 
1690                 xact => $circ->id,
1691                 billing_type => OILS_BILLING_TYPE_OVERDUE_MATERIALS
1692         )->gather(1);
1693
1694    if($bills) {
1695       for my $bill (@$bills) {
1696          $bill->voided('t');
1697          my $n = $bill->note || "";
1698          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1699          my $s = $session->request(
1700             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1701          return $U->DB_UPDATE_FAILED($bill) unless $s;
1702       }
1703    }
1704 }
1705
1706
1707
1708
1709
1710
1711 sub find_patron_from_copy {
1712         my $self = shift;
1713         my $circs = $self->editor->search_action_circulation(
1714                 { target_copy => $self->copy->id, checkin_time => undef });
1715         my $circ = $circs->[0];
1716         return unless $circ;
1717         my $u = $self->editor->retrieve_actor_user($circ->usr)
1718                 or return $self->bail_on_events($self->editor->event);
1719         $self->patron($u);
1720 }
1721
1722 sub check_checkin_copy_status {
1723         my $self = shift;
1724    my $copy = $self->copy;
1725
1726    my $islost     = 0;
1727    my $ismissing  = 0;
1728    my $evt        = undef;
1729
1730    my $status = $U->copy_status($copy->status)->id;
1731
1732    return undef
1733       if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
1734             $status == OILS_COPY_STATUS_CHECKED_OUT ||
1735             $status == OILS_COPY_STATUS_IN_PROCESS  ||
1736             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
1737             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
1738             $status == OILS_COPY_STATUS_CATALOGING  ||
1739             $status == OILS_COPY_STATUS_RESHELVING );
1740
1741    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1742       if( $status == OILS_COPY_STATUS_LOST );
1743
1744    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1745       if( $status == OILS_COPY_STATUS_MISSING );
1746
1747    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1748 }
1749
1750
1751
1752 # --------------------------------------------------------------------------
1753 # On checkin, we need to return as many relevant objects as we can
1754 # --------------------------------------------------------------------------
1755 sub checkin_flesh_events {
1756         my $self = shift;
1757
1758         if( grep { $_->{textcode} eq 'SUCCESS' } @{$self->events} 
1759                 and grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events} ) {
1760                         $self->events([grep { $_->{textcode} eq 'ITEM_NOT_CATALOGED' } @{$self->events}]);
1761         }
1762
1763
1764         for my $evt (@{$self->events}) {
1765
1766                 my $payload          = {};
1767                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1768                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1769                 $payload->{circ}     = $self->circ;
1770                 $payload->{transit}  = $self->transit;
1771                 $payload->{hold}     = $self->hold;
1772                 
1773                 $evt->{payload} = $payload;
1774         }
1775 }
1776
1777 sub log_me {
1778         my( $self, $msg ) = @_;
1779         my $bc = ($self->copy) ? $self->copy->barcode :
1780                 $self->barcode;
1781         $bc ||= "";
1782         my $usr = ($self->patron) ? $self->patron->id : "";
1783         $logger->info("circulator: $msg requestor=".$self->editor->requestor->id.
1784                 ", recipient=$usr, copy=$bc");
1785 }
1786
1787
1788 sub do_renew {
1789         my $self = shift;
1790         $self->log_me("do_renew()");
1791         $self->is_renewal(1);
1792
1793         unless( $self->is_renewal ) {
1794                 return $self->bail_on_events($self->editor->events)
1795                         unless $self->editor->allowed('RENEW_CIRC');
1796         }       
1797
1798         # Make sure there is an open circ to renew that is not
1799         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1800         my $circ = $self->editor->search_action_circulation(
1801                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1802
1803         if(!$circ) {
1804                 $circ = $self->editor->search_action_circulation(
1805                         { 
1806                                 target_copy => $self->copy->id, 
1807                                 stop_fines => OILS_STOP_FINES_MAX_FINES,
1808                                 checkin_time => undef
1809                         } 
1810                 )->[0];
1811         }
1812
1813         return $self->bail_on_events($self->editor->event) unless $circ;
1814
1815         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1816                 if $circ->renewal_remaining < 1;
1817
1818         # -----------------------------------------------------------------
1819
1820         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1821         $self->circ($circ);
1822
1823         $self->run_renew_permit;
1824
1825         # Check the item in
1826         $self->do_checkin();
1827         return if $self->bail_out;
1828
1829         unless( $self->permit_override ) {
1830                 $self->do_permit();
1831                 return if $self->bail_out;
1832                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1833                 $self->remove_event('ITEM_NOT_CATALOGED');
1834         }       
1835
1836         $self->override_events;
1837         return if $self->bail_out;
1838
1839         $self->events([]);
1840         $self->do_checkout();
1841 }
1842
1843
1844 sub remove_event {
1845         my( $self, $evt ) = @_;
1846         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1847         $logger->debug("circulator: removing event from list: $evt");
1848         my @events = @{$self->events};
1849         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1850 }
1851
1852
1853 sub have_event {
1854         my( $self, $evt ) = @_;
1855         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1856         return grep { $_->{textcode} eq $evt } @{$self->events};
1857 }
1858
1859
1860
1861 sub run_renew_permit {
1862         my $self = shift;
1863    my $runner = $self->script_runner;
1864
1865    $runner->load($self->circ_permit_renew);
1866    my $result = $runner->run or 
1867                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1868    my $events = $result->{events};
1869
1870    $logger->activity("ciculator: circ_permit_renew for user ".
1871       $self->patron->id." returned events: @$events") if @$events;
1872
1873         $self->push_events(OpenILS::Event->new($_)) for @$events;
1874         
1875         $logger->debug("circulator: re-creating script runner to be safe");
1876         $self->mk_script_runner;
1877 }
1878
1879
1880
1881