]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
better handling of stop_fines items
[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         unless( $self->editor->requestor->id == $self->patron->id ) {
480                 return $self->bail_on_events($self->editor->event)
481                         unless( $self->editor->allowed('VIEW_PERMIT_CHECKOUT') );
482         }
483
484         $self->do_copy_checks();
485         return if $self->bail_out;
486         $self->run_patron_permit_scripts();
487         $self->run_copy_permit_scripts() 
488                 unless $self->is_precat or $self->is_noncat;
489         $self->override_events() unless $self->is_renewal;
490         return if $self->bail_out;
491
492         if( $self->is_precat ) {
493                 $self->push_events(
494                         OpenILS::Event->new(
495                                 'ITEM_NOT_CATALOGED', payload => $self->mk_permit_key));
496                 return $self->bail_out(1) unless $self->is_renewal;
497         }
498
499         $self->push_events(
500       OpenILS::Event->new(
501                         'SUCCESS', 
502                         payload => $self->mk_permit_key));
503 }
504
505
506 sub do_copy_checks {
507         my $self = shift;
508         my $copy = $self->copy;
509         return unless $copy;
510
511         my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
512
513         # We cannot check out a copy if it is in-transit
514         if( $stat == $U->copy_status_from_name('in transit')->id ) {
515                 return $self->bail_on_events(OpenILS::Event->new('COPY_IN_TRANSIT'));
516         }
517
518         $self->handle_claims_returned();
519         return if $self->bail_out;
520
521         # no claims returned circ was found, check if there is any open circ
522         unless( $self->is_renewal ) {
523                 my $circs = $self->editor->search_action_circulation(
524                         { target_copy => $copy->id, stop_fines_time => undef }
525                 );
526
527                 return $self->bail_on_events(
528                         OpenILS::Event->new('OPEN_CIRCULATION_EXISTS')) if @$circs;
529         }
530 }
531
532
533 # ---------------------------------------------------------------------
534 # This pushes any patron-related events into the list but does not
535 # set bail_out for any events
536 # ---------------------------------------------------------------------
537 sub run_patron_permit_scripts {
538         my $self                = shift;
539         my $runner              = $self->script_runner;
540         my $patronid    = $self->patron->id;
541
542         # ---------------------------------------------------------------------
543         # Find all of the fatal penalties currently set on the user
544         # ---------------------------------------------------------------------
545         my $penalties = $U->update_patron_penalties( 
546                 authtoken => $self->editor->authtoken,
547                 patron    => $self->patron,
548         );
549
550         $penalties = $penalties->{fatal_penalties};
551
552         # ---------------------------------------------------------------------
553         # Now run the patron permit script 
554         # ---------------------------------------------------------------------
555         $runner->load($self->circ_permit_patron);
556         my $result = $runner->run or 
557                 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
558
559         my $patron_events = $result->{events};
560         my @allevents; 
561         push( @allevents, OpenILS::Event->new($_)) for (@$penalties, @$patron_events);
562
563         $logger->info("circulator: permit_patron script returned events: @allevents") if @allevents;
564
565         $self->push_events(@allevents);
566 }
567
568
569 sub run_copy_permit_scripts {
570         my $self = shift;
571         my $copy = $self->copy || return;
572         my $runner = $self->script_runner;
573         
574    # ---------------------------------------------------------------------
575    # Capture all of the copy permit events
576    # ---------------------------------------------------------------------
577    $runner->load($self->circ_permit_copy);
578    my $result = $runner->run or 
579                 throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
580    my $copy_events = $result->{events};
581
582    # ---------------------------------------------------------------------
583    # Now collect all of the events together
584    # ---------------------------------------------------------------------
585         my @allevents;
586    push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
587
588         # See if this copy has an alert message
589         my $ae = $self->check_copy_alert();
590         push( @allevents, $ae ) if $ae;
591
592    # uniquify the events
593    my %hash = map { ($_->{ilsevent} => $_) } @allevents;
594    @allevents = values %hash;
595
596
597         # If the script says the copy is not available, put the status
598         # in as the payload for that event
599         my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
600    for (@allevents) {
601       $_->{payload} = $stat if 
602                         ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
603    }
604
605         $logger->info("circulator: permit_copy script returned events: @allevents") if @allevents;
606
607         $self->push_events(@allevents);
608 }
609
610
611 sub check_copy_alert {
612         my $self = shift;
613         return OpenILS::Event->new(
614                 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
615                 if $self->copy and $self->copy->alert_message;
616         return undef;
617 }
618
619
620
621 # --------------------------------------------------------------------------
622 # If the call is overriding and has permissions to override every collected
623 # event, the are cleared.  Any event that the caller does not have
624 # permission to override, will be left in the event list and bail_out will
625 # be set
626 # XXX We need code in here to cancel any holds/transits on copies 
627 # that are being force-checked out
628 # --------------------------------------------------------------------------
629 sub override_events {
630         my $self = shift;
631         my @events = @{$self->events};
632         return unless @events;
633
634         if(!$self->override) {
635                 return $self->bail_out(1) 
636                         if( @events > 1 or $events[0]->{textcode} ne 'SUCCESS' );
637         }       
638
639         $self->events([]);
640         
641    for my $e (@events) {
642       my $tc = $e->{textcode};
643       next if $tc eq 'SUCCESS';
644       my $ov = "$tc.override";
645       $logger->info("circulator: attempting to override event: $ov");
646
647                 return $self->bail_on_events($self->editor->event)
648                         unless( $self->editor->allowed($ov)     );
649    }
650 }
651         
652
653 # --------------------------------------------------------------------------
654 # If there is an open claimsreturn circ on the requested copy, close the 
655 # circ if overriding, otherwise bail out
656 # --------------------------------------------------------------------------
657 sub handle_claims_returned {
658         my $self = shift;
659         my $copy = $self->copy;
660
661         my $CR = $self->editor->search_action_circulation(
662                 {       
663                         target_copy             => $copy->id,
664                         stop_fines              => 'CLAIMSRETURNED',
665                         checkin_time    => undef,
666                 }
667         );
668
669         return unless ($CR = $CR->[0]); 
670
671         my $evt;
672
673         # - If the caller has set the override flag, we will check the item in
674         if($self->override) {
675
676                 $CR->checkin_time('now');       
677                 $CR->checkin_lib($self->editor->requestor->ws_ou);
678                 $CR->checkin_staff($self->editor->requestor->id);
679
680                 $evt = $self->editor->event 
681                         unless $self->editor->update_action_circulation($CR);
682
683         } else {
684                 $evt = OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
685         }
686
687         $self->bail_on_events($evt) if $evt;
688         return;
689 }
690
691
692 # --------------------------------------------------------------------------
693 # This performs the checkout
694 # --------------------------------------------------------------------------
695 sub do_checkout {
696         my $self = shift;
697
698         # make sure perms are good if this isn't a renewal
699         unless( $self->is_renewal ) {
700                 return $self->bail_on_events($self->editor->event)
701                         unless( $self->editor->allowed('COPY_CHECKOUT') );
702         }
703
704         # verify the permit key
705         unless( $self->check_permit_key ) {
706                 if( $self->permit_override ) {
707                         return $self->bail_on_events($self->editor->event)
708                                 unless $self->editor->allowed('CIRC_PERMIT_OVERRIDE');
709                 } else {
710                         return $self->bail_on_events(OpenILS::Event->new('CIRC_PERMIT_BAD_KEY'))
711                 }       
712         }
713
714         # if this is a non-cataloged circ, build the circ and finish
715         if( $self->is_noncat ) {
716                 $self->checkout_noncat;
717                 $self->push_events(
718                         OpenILS::Event->new('SUCCESS', 
719                         payload => { noncat_circ => $self->circ }));
720                 return;
721         }
722
723         if( $self->is_precat ) {
724                 $self->script_runner->insert("environment.isPrecat", 1, 1);
725                 $self->make_precat_copy;
726                 return if $self->bail_out;
727
728         } elsif( $self->copy->call_number == -1 ) {
729                 return $self->bail_on_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
730         }
731
732         $self->do_copy_checks;
733         return if $self->bail_out;
734
735         $self->run_checkout_scripts();
736         return if $self->bail_out;
737
738         $self->build_checkout_circ_object();
739         return if $self->bail_out;
740
741         $self->apply_modified_due_date();
742         return if $self->bail_out;
743
744         return $self->bail_on_events($self->editor->event)
745                 unless $self->editor->create_action_circulation($self->circ);
746
747         $self->copy->status($U->copy_status_from_name('checked out'));
748         $self->update_copy;
749         return if $self->bail_out;
750
751         $self->handle_checkout_holds();
752         return if $self->bail_out;
753
754    # ------------------------------------------------------------------------------
755    # Update the patron penalty info in the DB
756    # ------------------------------------------------------------------------------
757    $U->update_patron_penalties(
758       authtoken => $self->editor->authtoken,
759       patron    => $self->patron,
760       background  => 1,
761    );
762
763         my $record = $U->record_to_mvr($self->title) unless $self->is_precat;
764         $self->push_events(
765                 OpenILS::Event->new('SUCCESS',
766                         payload  => {
767                                 copy              => $U->unflesh_copy($self->copy),
768                                 circ              => $self->circ,
769                                 record            => $record,
770                                 holds_fulfilled   => $self->fulfilled_holds,
771                         }
772                 )
773         );
774 }
775
776 sub update_copy {
777         my $self = shift;
778         my $copy = $self->copy;
779
780         my $stat = $copy->status if ref $copy->status;
781         my $loc = $copy->location if ref $copy->location;
782         my $circ_lib = $copy->circ_lib if ref $copy->circ_lib;
783
784         $copy->status($stat->id) if $stat;
785         $copy->location($loc->id) if $loc;
786         $copy->circ_lib($circ_lib->id) if $circ_lib;
787
788         return $self->bail_on_events($self->editor->event)
789                 unless $self->editor->update_asset_copy($self->copy);
790
791         $copy->status($stat) if $stat;
792         $copy->location($loc) if $loc;
793         $copy->circ_lib($circ_lib) if $circ_lib;
794 }
795
796
797 sub bail_on_events {
798         my( $self, @evts ) = @_;
799         $self->push_events(@evts);
800         $self->bail_out(1);
801 }
802
803 sub handle_checkout_holds {
804    my $self    = shift;
805
806    my $copy    = $self->copy;
807    my $patron  = $self->patron;
808         my $holds       = $self->editor->search_action_hold_request(
809                 { current_copy =>  $copy->id , fulfillment_time => undef });
810
811    my @fulfilled;
812
813    # XXX We should only fulfill one hold here...
814    # XXX If a hold was transited to the user who is checking out
815    # the item, we need to make sure that hold is what's grabbed
816    if(@$holds) {
817
818       # for now, just sort by id to get what should be the oldest hold
819       $holds = [ sort { $a->id <=> $b->id } @$holds ];
820       my @myholds = grep { $_->usr eq $patron->id } @$holds;
821       my @altholds   = grep { $_->usr ne $patron->id } @$holds;
822
823       if(@myholds) {
824          my $hold = $myholds[0];
825
826          $logger->debug("Related hold found in checkout: " . $hold->id );
827
828          $hold->current_copy($copy->id); # just make sure it's set
829          # if the hold was never officially captured, capture it.
830          $hold->capture_time('now') unless $hold->capture_time;
831          $hold->fulfillment_time('now');
832                         return $self->bail_on_events($self->editor->event)
833                                 unless $self->editor->update_action_hold_request($hold);
834
835          push( @fulfilled, $hold->id );
836       }
837
838       # If there are any holds placed for other users that point to this copy,
839       # then we need to un-target those holds so the targeter can pick a new copy
840       for(@altholds) {
841
842          $logger->info("Un-targeting hold ".$_->id.
843             " because copy ".$copy->id." is getting checked out");
844
845          $_->clear_current_copy;
846                         return $self->bail_on_event($self->editor->event)
847                                 unless $self->editor->update_action_hold_request($_);
848       }
849    }
850
851         $self->fulfilled_holds(\@fulfilled);
852 }
853
854
855
856 sub run_checkout_scripts {
857         my $self = shift;
858
859         my $evt;
860    my $runner = $self->script_runner;
861    $runner->load($self->circ_duration);
862
863    my $result = $runner->run or 
864                 throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
865
866    my $duration   = $result->{durationRule};
867    my $dur_level  = $result->{durationLevel};
868    my $recurring  = $result->{recurringFinesRule};
869    my $max_fine   = $result->{maxFine};
870    my $rec_fines_level = $result->{recurringFinesLevel};
871
872    ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
873         return $self->bail_on_events($evt) if $evt;
874    ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
875         return $self->bail_on_events($evt) if $evt;
876    ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
877         return $self->bail_on_events($evt) if $evt;
878
879    $self->duration_level($dur_level);
880    $self->recurring_fines_level($rec_fines_level);
881    $self->duration_rule($duration);
882    $self->recurring_fines_rule($recurring);
883    $self->max_fine_rule($max_fine);
884 }
885
886
887 sub build_checkout_circ_object {
888         my $self = shift;
889
890    my $circ       = Fieldmapper::action::circulation->new;
891    my $duration   = $self->duration_rule;
892    my $max        = $self->max_fine_rule;
893    my $recurring  = $self->recurring_fines_rule;
894    my $copy       = $self->copy;
895    my $patron     = $self->patron;
896    my $dur_level  = $self->duration_level;
897    my $rec_level  = $self->recurring_fines_level;
898
899    $circ->duration( $duration->shrt ) if ($dur_level == 1);
900    $circ->duration( $duration->normal ) if ($dur_level == 2);
901    $circ->duration( $duration->extended ) if ($dur_level == 3);
902
903    $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
904    $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
905    $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
906
907    $circ->duration_rule( $duration->name );
908    $circ->recuring_fine_rule( $recurring->name );
909    $circ->max_fine_rule( $max->name );
910    $circ->max_fine( $max->amount );
911
912    $circ->fine_interval($recurring->recurance_interval);
913    $circ->renewal_remaining( $duration->max_renewals );
914    $circ->target_copy( $copy->id );
915    $circ->usr( $patron->id );
916    $circ->circ_lib( $self->circ_lib );
917
918    if( $self->is_renewal ) {
919       $circ->opac_renewal(1);
920       $circ->renewal_remaining($self->renewal_remaining);
921       $circ->circ_staff($self->editor->requestor->id);
922    }
923
924
925    # if the user provided an overiding checkout time,
926    # (e.g. the checkout really happened several hours ago), then
927    # we apply that here.  Does this need a perm??
928         $circ->xact_start(clense_ISO8601($self->checkout_time))
929                 if $self->checkout_time;
930
931    # if a patron is renewing, 'requestor' will be the patron
932    $circ->circ_staff($self->editor->requestor->id);
933         $circ->due_date( $self->create_due_date($circ->duration) );
934
935         $self->circ($circ);
936 }
937
938
939 sub apply_modified_due_date {
940         my $self = shift;
941         my $circ = $self->circ;
942         my $copy = $self->copy;
943
944    if( $self->due_date ) {
945
946                 return $self->bail_on_events($self->editor->event)
947                         unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
948
949       $circ->due_date(clense_ISO8601($self->due_date));
950
951    } else {
952
953       # if the due_date lands on a day when the location is closed
954       return unless $copy;
955
956                 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
957
958       $logger->info("circ searching for closed date overlap on lib $org".
959                         " with an item due date of ".$circ->due_date );
960
961       my $dateinfo = $U->storagereq(
962          'open-ils.storage.actor.org_unit.closed_date.overlap', 
963                         $org, $circ->due_date );
964
965       if($dateinfo) {
966          $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
967             $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
968
969             # XXX make the behavior more dynamic
970             # for now, we just push the due date to after the close date
971             $circ->due_date($dateinfo->{end});
972       }
973    }
974 }
975
976
977
978 sub create_due_date {
979         my( $self, $duration ) = @_;
980    my ($sec,$min,$hour,$mday,$mon,$year) =
981       gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
982    $year += 1900; $mon += 1;
983    my $due_date = sprintf(
984       '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
985       $year, $mon, $mday, $hour, $min, $sec);
986    return $due_date;
987 }
988
989
990
991 sub make_precat_copy {
992         my $self = shift;
993         my $copy = $self->copy;
994
995    if($copy) {
996       $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
997
998       $copy->editor($self->editor->requestor->id);
999       $copy->edit_date('now');
1000       $copy->dummy_title($self->dummy_title);
1001       $copy->dummy_author($self->dummy_author);
1002
1003                 $self->update_copy();
1004                 return;
1005    }
1006
1007    $logger->info("circulator: Creating a new precataloged ".
1008                 "copy in checkout with barcode " . $self->copy_barcode);
1009
1010    $copy = Fieldmapper::asset::copy->new;
1011    $copy->circ_lib($self->circ_lib);
1012    $copy->creator($self->editor->requestor->id);
1013    $copy->editor($self->editor->requestor->id);
1014    $copy->barcode($self->copy_barcode);
1015    $copy->call_number(-1); #special CN for precat materials
1016    $copy->loan_duration(&PRECAT_LOAN_DURATION);
1017    $copy->fine_level(&PRECAT_FINE_LEVEL);
1018
1019    $copy->dummy_title($self->dummy_title || "");
1020    $copy->dummy_author($self->dummy_author || "");
1021
1022         unless( $self->copy($self->editor->create_asset_copy($copy)) ) {
1023                 $self->bail_out(1);
1024                 $self->push_events($self->editor->event);
1025                 return;
1026         }       
1027
1028         # this is a little bit of a hack, but we need to 
1029         # get the copy into the script runner
1030         $self->script_runner->insert("environment.copy", $copy, 1);
1031 }
1032
1033
1034 sub checkout_noncat {
1035         my $self = shift;
1036
1037         my $circ;
1038         my $evt;
1039
1040    my $lib              = $self->noncat_circ_lib || $self->editor->requestor->ws_ou;
1041    my $count    = $self->noncat_count || 1;
1042    my $cotime   = clense_ISO8601($self->checkout_time) || "";
1043
1044    $logger->info("circ creating $count noncat circs with checkout time $cotime");
1045
1046    for(1..$count) {
1047
1048       ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1049          $self->editor->requestor->id, 
1050                         $self->patron->id, 
1051                         $lib, 
1052                         $self->noncat_type, 
1053                         $cotime,
1054                         $self->editor );
1055
1056                 if( $evt ) {
1057                         $self->push_events($evt);
1058                         $self->bail_out(1);
1059                         return; 
1060                 }
1061                 $self->circ($circ);
1062    }
1063 }
1064
1065
1066 sub do_checkin {
1067         my $self = shift;
1068
1069         unless( $self->is_renewal ) {
1070                 return $self->bail_on_events($self->editor->event)
1071                         unless $self->editor->allowed('COPY_CHECKIN');
1072         }
1073
1074         $self->push_events($self->check_copy_alert());
1075         $self->push_events($self->check_checkin_copy_status());
1076
1077
1078         # the renew code will have already found our circulation object
1079         unless( $self->is_renewal and $self->circ ) {
1080
1081                 # first lets see if we have a good old fashioned open circulation
1082                 my $circ = $self->editor->search_action_circulation(
1083                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1084
1085                 if(!$circ) {
1086                         # if not, lets look for other circs we can check in
1087                         $circ = $self->editor->search_action_circulation(
1088                                 { 
1089                                         target_copy => $self->copy->id, 
1090                                         xact_finish => undef,
1091                                         stop_fines      => [ 'CLAIMSRETURNED', 'LOST', 'LONGOVERDUE' ]
1092                                 } )->[0];
1093                 }
1094
1095                 $self->circ($circ);
1096         }
1097
1098
1099         # if the circ is marked as 'claims returned', add the event to the list
1100         $self->push_events(OpenILS::Event->new('CIRC_CLAIMS_RETURNED'))
1101                 if ($self->circ and $self->circ->stop_fines 
1102                                 and $self->circ->stop_fines eq 'CLAIMSRETURNED');
1103
1104         # handle the overridable events 
1105         $self->override_events unless $self->is_renewal;
1106         
1107         if( $self->copy ) {
1108                 $self->transit(
1109                         $self->editor->search_action_transit_copy(
1110                         { target_copy => $self->copy->id, dest_recv_time => undef })->[0]);     
1111         }
1112
1113         if( $self->circ ) {
1114                 $self->checkin_handle_circ;
1115                 return if $self->bail_out;
1116                 $self->checkin_changed(1);
1117
1118         } elsif( $self->transit ) {
1119                 my $hold_transit = $self->process_received_transit;
1120                 $self->checkin_changed(1);
1121
1122                 if( $self->bail_out ) { 
1123                         $self->checkin_flesh_events;
1124                         return;
1125                 }
1126                 
1127                 if( my $e = $self->check_checkin_copy_status() ) {
1128                         # If the original copy status is special, alert the caller
1129                         return $self->bail_on_events($e);       
1130                 }
1131
1132                 if( $hold_transit ) {
1133                         $self->checkin_flesh_events;
1134                         return;
1135                 } 
1136         }
1137
1138         if( $self->is_renewal ) {
1139                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1140                 return;
1141         }
1142
1143    # ------------------------------------------------------------------------------
1144    # Circulations and transits are now closed where necessary.  Now go on to see if
1145    # this copy can fulfill a hold or needs to be routed to a different location
1146    # ------------------------------------------------------------------------------
1147
1148         if( $self->attempt_checkin_hold_capture() ) {
1149                 return if $self->bail_out;
1150
1151    } else { # not needed for a hold
1152
1153                 my $circ_lib = (ref $self->copy->circ_lib) ? 
1154                                 $self->copy->circ_lib->id : $self->copy->circ_lib;
1155
1156                 $logger->debug("circulator: circlib=$circ_lib, workstation=".$self->editor->requestor->ws_ou);
1157
1158       if( $circ_lib == $self->editor->requestor->ws_ou ) {
1159
1160                         $self->checkin_handle_precat();
1161                         return if $self->bail_out;
1162
1163       } else {
1164
1165                         $self->checkin_build_copy_transit();
1166                         return if $self->bail_out;
1167                         $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
1168       }
1169    }
1170
1171
1172         $self->reshelve_copy;
1173         return if $self->bail_out;
1174
1175         unless($self->checkin_changed) {
1176
1177                 $self->push_events(OpenILS::Event->new('NO_CHANGE'));
1178                 my $stat = (ref $self->copy->status) ? $self->copy->status->id : $self->copy->status;
1179
1180         $self->hold($U->fetch_open_hold_by_copy($self->copy->id))
1181          if( $stat == $U->copy_status_from_name('on holds shelf')->id );
1182                 $self->bail_out(1); # no need to commit anything
1183
1184         } else {
1185                 $self->push_events(OpenILS::Event->new('SUCCESS')) 
1186                         unless @{$self->events};
1187         }
1188
1189         $self->checkin_flesh_events;
1190         return;
1191 }
1192
1193 sub reshelve_copy {
1194    my $self    = shift;
1195    my $copy    = $self->copy;
1196    my $force   = $self->force;
1197
1198    my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1199
1200    if($force || (
1201       $stat != $U->copy_status_from_name('on holds shelf')->id and
1202       $stat != $U->copy_status_from_name('available')->id and
1203       $stat != $U->copy_status_from_name('cataloging')->id and
1204       $stat != $U->copy_status_from_name('in transit')->id and
1205       $stat != $U->copy_status_from_name('reshelving')->id) ) {
1206
1207         $copy->status( $U->copy_status_from_name('reshelving') );
1208                         $self->update_copy;
1209                         $self->checkin_changed(1);
1210         }
1211 }
1212
1213
1214 sub checkin_handle_precat {
1215         my $self        = shift;
1216    my $copy    = $self->copy;
1217    my $catstat = $U->copy_status_from_name('cataloging');
1218
1219    if( $self->is_precat and ($copy->status != $catstat->id) ) {
1220       $copy->status($catstat);
1221                 $self->update_copy();
1222                 $self->checkin_changed(1);
1223                 $self->push_events(OpenILS::Event->new('ITEM_NOT_CATALOGED'));
1224    }
1225 }
1226
1227
1228 sub checkin_build_copy_transit {
1229         my $self                        = shift;
1230    my $copy       = $self->copy;
1231    my $transit    = Fieldmapper::action::transit_copy->new;
1232
1233    $transit->source($self->editor->requestor->ws_ou);
1234    $transit->dest( (ref($copy->circ_lib)) ? $copy->circ_lib->id : $copy->circ_lib );
1235    $transit->target_copy($copy->id);
1236    $transit->source_send_time('now');
1237    $transit->copy_status( (ref $copy->status) ? $copy->status->id : $copy->status );
1238
1239         return $self->bail_on_events($self->editor->event)
1240                 unless $self->editor->create_action_transit_copy($transit);
1241
1242    $copy->status($U->copy_status_from_name('in transit'));
1243         $self->update_copy;
1244         $self->checkin_changed(1);
1245 }
1246
1247
1248 sub attempt_checkin_hold_capture {
1249         my $self = shift;
1250         my $copy = $self->copy;
1251
1252         # See if this copy can fulfill any holds
1253         my ($hold) = $holdcode->find_nearest_permitted_hold(
1254                 OpenSRF::AppSession->create('open-ils.storage'), 
1255                 $copy, $self->editor->requestor );
1256
1257         if(!$hold) {
1258                 $logger->debug("circulator: no potential permitted".
1259                         "holds found for copy ".$copy->barcode);
1260                 return undef;
1261         }
1262
1263         $logger->info("circulator: found permitted hold ".
1264                 $hold->id . " for copy, capturing...");
1265
1266         $hold->current_copy($copy->id);
1267         $hold->capture_time('now');
1268
1269         # prevent some DB errors
1270         $hold->clear_fulfillment_time;
1271         $hold->clear_fulfillment_staff;
1272         $hold->clear_fulfillment_lib;
1273         $hold->clear_expire_time; 
1274
1275         $self->bail_on_events($self->editor->event)
1276                 unless $self->editor->update_action_hold_request($hold);
1277         $self->hold($hold);
1278         $self->checkin_changed(1);
1279
1280         return 1 if $self->bail_out;
1281
1282         if( $hold->pickup_lib == $self->editor->requestor->ws_ou ) {
1283
1284                 # This hold was captured in the correct location
1285         $copy->status( $U->copy_status_from_name('on holds shelf') );
1286                 $self->push_events(OpenILS::Event->new('SUCCESS'));
1287         
1288         } else {
1289         
1290                 # Hold needs to be picked up elsewhere.  Build a hold
1291                 # transit and route the item.
1292                 $self->checkin_build_hold_transit();
1293         $copy->status($U->copy_status_from_name('in transit') );
1294                 return 1 if $self->bail_out;
1295                 $self->push_events(
1296                         OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib));
1297         }
1298
1299         # make sure we save the copy status
1300         $self->update_copy;
1301         return 1;
1302 }
1303
1304
1305 sub checkin_build_hold_transit {
1306         my $self = shift;
1307
1308    my $copy = $self->copy;
1309    my $hold = $self->hold;
1310    my $trans = Fieldmapper::action::hold_transit_copy->new;
1311
1312         my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
1313    $trans->hold($hold->id);
1314    $trans->source($self->editor->requestor->ws_ou);
1315    $trans->dest($hold->pickup_lib);
1316    $trans->source_send_time("now");
1317    $trans->target_copy($copy->id);
1318    $trans->copy_status($stat);
1319
1320         return $self->bail_on_events($self->editor->event)
1321                 unless $self->editor->create_action_hold_transit_copy($trans);
1322 }
1323
1324
1325
1326 sub process_received_transit {
1327         my $self = shift;
1328         my $copy = $self->copy;
1329    my $copyid = $self->copy->id;
1330
1331    my $status_name = $U->copy_status_to_name($copy->status);
1332    $logger->debug("circulator: attempting transit receive on ".
1333                 "copy $copyid. Copy status is $status_name");
1334
1335         my $transit = $self->transit;
1336
1337    if( $transit->dest != $self->editor->requestor->ws_ou ) {
1338       $logger->activity("Fowarding transit on copy which is destined ".
1339          "for a different location. copy=$copyid,current ".
1340          "location=".$self->editor->requestor->ws_ou.",destination location=".$transit->dest);
1341
1342                 $self->bail_on_events(
1343                         OpenILS::Event->new('ROUTE_ITEM', org => $transit->dest ));
1344    }
1345
1346    # The transit is received, set the receive time
1347    $transit->dest_recv_time('now');
1348         $self->bail_on_events($self->editor->event)
1349                 unless $self->editor->update_action_transit_copy($transit);
1350
1351         my $hold_transit = $self->editor->search_action_hold_transit_copy(
1352                 { hold => $transit->id }
1353         );
1354
1355    $logger->info("Recovering original copy status in transit: ".$transit->copy_status);
1356    $copy->status( $transit->copy_status );
1357         $self->update_copy();
1358         return if $self->bail_out;
1359
1360         my $ishold = ($hold_transit) ? 1 : 0;
1361
1362         $self->push_events( 
1363                 OpenILS::Event->new(
1364                 'SUCCESS', 
1365                 ishold => $ishold,
1366       payload => { transit => $transit, holdtransit => $hold_transit } ));
1367
1368         return $hold_transit;
1369 }
1370
1371
1372 sub checkin_handle_circ {
1373    my $self = shift;
1374         $U->logmark;
1375
1376    my $circ = $self->circ;
1377    my $copy = $self->copy;
1378    my $evt;
1379    my $obt;
1380
1381    # backdate the circ if necessary
1382    if($self->backdate) {
1383                 $self->handle_backdate;
1384                 return if $self->bail_out;
1385    }
1386
1387    if(!$circ->stop_fines) {
1388       $circ->stop_fines('CHECKIN');
1389       $circ->stop_fines('RENEW') if $self->is_renewal;
1390       $circ->stop_fines_time('now');
1391    }
1392
1393    # see if there are any fines owed on this circ.  if not, close it
1394         $obt = $self->editor->retrieve_money_open_billable_transaction_summary($circ->id);
1395    $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1396
1397    # Set the checkin vars since we have the item
1398    $circ->checkin_time('now');
1399    $circ->checkin_staff($self->editor->requestor->id);
1400    $circ->checkin_lib($self->editor->requestor->ws_ou);
1401
1402         $self->copy->status($U->copy_status_from_name('reshelving'));
1403         $self->update_copy;
1404
1405         return $self->bail_on_events($self->editor->event)
1406                 unless $self->editor->update_action_circulation($circ);
1407 }
1408
1409
1410 sub checkin_handle_backdate {
1411         my $self = shift;
1412
1413         my $bills = $self->editor->search_money_billing(
1414                 { billing_ts => { ">=" => $self->backdate }, "xact" => $self->circ->id }
1415         );
1416
1417         for my $bill (@$bills) {        
1418                 if( !$bill->voided or $bill->voided =~ /f/i ) {
1419                         $bill->voided('t');
1420                         my $n = $bill->note || "";
1421                         $bill->note("$n\nSystem: VOIDED FOR BACKDATE");
1422
1423                         $self->bail_on_events($self->editor->event)
1424                                 unless $self->editor->update_money_billing($bill);
1425                 }
1426         }
1427 }
1428
1429
1430
1431 # XXX Legacy version for Circ.pm support
1432 sub _checkin_handle_backdate {
1433    my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1434
1435    my $bills = $session->request(
1436       "open-ils.storage.direct.money.billing.search_where.atomic",
1437       billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1438
1439    if($bills) {
1440       for my $bill (@$bills) {
1441          $bill->voided('t');
1442          my $n = $bill->note || "";
1443          $bill->note($n . "\nSystem: VOIDED FOR BACKDATE");
1444          my $s = $session->request(
1445             "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1446          return $U->DB_UPDATE_FAILED($bill) unless $s;
1447       }
1448    }
1449 }
1450
1451
1452
1453
1454
1455
1456 sub find_patron_from_copy {
1457         my $self = shift;
1458         my $circs = $self->editor->search_action_circulation(
1459                 { target_copy => $self->copy->id, stop_fines_time => undef });
1460         my $circ = $circs->[0];
1461         return unless $circ;
1462         my $u = $self->editor->retrieve_actor_user($circ->usr)
1463                 or return $self->bail_on_events($self->editor->event);
1464         $self->patron($u);
1465 }
1466
1467 sub check_checkin_copy_status {
1468         my $self = shift;
1469    my $copy = $self->copy;
1470
1471    my $islost     = 0;
1472    my $ismissing  = 0;
1473    my $evt        = undef;
1474
1475    my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1476
1477    return undef
1478       if(   $status == $U->copy_status_from_name('available')->id    ||
1479             $status == $U->copy_status_from_name('checked out')->id  ||
1480             $status == $U->copy_status_from_name('in process')->id   ||
1481             $status == $U->copy_status_from_name('in transit')->id   ||
1482             $status == $U->copy_status_from_name('reshelving')->id );
1483
1484    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1485       if( $status == $U->copy_status_from_name('lost')->id );
1486
1487    return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1488       if( $status == $U->copy_status_from_name('missing')->id );
1489
1490    return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1491 }
1492
1493
1494
1495 # --------------------------------------------------------------------------
1496 # On checkin, we need to return as many relevant objects as we can
1497 # --------------------------------------------------------------------------
1498 sub checkin_flesh_events {
1499         my $self = shift;
1500
1501         for my $evt (@{$self->events}) {
1502
1503                 my $payload          = {};
1504                 $payload->{copy}     = $U->unflesh_copy($self->copy);
1505                 $payload->{record}   = $U->record_to_mvr($self->title) if($self->title and !$self->is_precat);
1506                 $payload->{circ}     = $self->circ;
1507                 $payload->{transit}  = $self->transit;
1508                 $payload->{hold}     = $self->hold;
1509                 
1510                 $evt->{payload} = $payload;
1511         }
1512 }
1513
1514
1515 sub do_renew {
1516         my $self = shift;
1517         $self->is_renewal(1);
1518
1519         #$self->find_patron_from_copy unless $self->patron;
1520
1521         unless( $self->is_renewal ) {
1522                 return $self->bail_on_events($self->editor->events)
1523                         unless $self->editor->allowed('RENEW_CIRC');
1524         }       
1525
1526         # Make sure there is an open circ to renew that is not
1527         # marked as LOST, CLAIMSRETURNED, or LONGOVERDUE
1528         my $circ = $self->editor->search_action_circulation(
1529                         { target_copy => $self->copy->id, stop_fines => undef } )->[0];
1530
1531         return $self->bail_on_events($self->editor->event) unless $circ;
1532
1533         $self->push_events(OpenILS::Event->new('MAX_RENEWALS_REACHED'))
1534                 if $circ->renewal_remaining < 1;
1535
1536         # -----------------------------------------------------------------
1537
1538         $self->renewal_remaining( $circ->renewal_remaining - 1 );
1539         $self->renewal_remaining(0) if $self->renewal_remaining < 0;
1540         $self->circ($circ);
1541
1542         $self->run_renew_permit;
1543
1544         # Check the item in
1545         $self->do_checkin();
1546         return if $self->bail_out;
1547
1548         unless( $self->permit_override ) {
1549                 $self->do_permit();
1550                 return if $self->bail_out;
1551                 $self->is_precat(1) if $self->have_event('ITEM_NOT_CATALOGED');
1552                 $self->remove_event('ITEM_NOT_CATALOGED');
1553         }       
1554
1555         $self->override_events;
1556         return if $self->bail_out;
1557
1558         $self->events([]);
1559         $self->do_checkout();
1560 }
1561
1562
1563 sub remove_event {
1564         my( $self, $evt ) = @_;
1565         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1566         $logger->debug("circulator: removing event from list: $evt");
1567         my @events = @{$self->events};
1568         $self->events( [ grep { $_->{textcode} ne $evt } @events ] );
1569 }
1570
1571
1572 sub have_event {
1573         my( $self, $evt ) = @_;
1574         $evt = (ref $evt) ? $evt->{textcode} : $evt;
1575         return grep { $_->{textcode} eq $evt } @{$self->events};
1576 }
1577
1578
1579
1580 sub run_renew_permit {
1581         my $self = shift;
1582    my $runner = $self->script_runner;
1583
1584    $runner->load($self->circ_permit_renew);
1585    my $result = $runner->run or 
1586                 throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1587    my $events = $result->{events};
1588
1589    $logger->activity("circ_permit_renew for user ".
1590       $self->patron->id." returned events: @$events") if @$events;
1591
1592         $self->push_events(OpenILS::Event->new($_)) for @$events;
1593 }
1594
1595
1596
1597