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