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