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