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