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