]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
added renew.override
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Circulate.pm
1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
5 use Data::Dumper;
6 use OpenSRF::Utils::Cache;
7 use OpenSRF::AppSession;
8 use Digest::MD5 qw(md5_hex);
9 use OpenILS::Utils::ScriptRunner;
10 use OpenILS::Application::AppUtils;
11 use OpenILS::Application::Circ::Holds;
12 use OpenILS::Application::Circ::Transit;
13 use OpenILS::Utils::PermitHold;
14 use OpenSRF::Utils::Logger qw(:logger);
15 use DateTime;
16 use DateTime::Format::ISO8601;
17 use OpenSRF::Utils qw/:datetime/;
18
19 $Data::Dumper::Indent = 0;
20 my $apputils    = "OpenILS::Application::AppUtils";
21 my $U                           = $apputils;
22 my $holdcode    = "OpenILS::Application::Circ::Holds";
23 my $transcode   = "OpenILS::Application::Circ::Transit";
24
25 my %scripts;                    # - circulation script filenames
26 my $script_libs;                # - any additional script libraries
27 my %cache;                              # - db objects cache
28 my %contexts;                   # - Script runner contexts
29 my $cache_handle;               # - memcache handle
30
31 sub PRECAT_FINE_LEVEL { return 2; }
32 sub PRECAT_LOAN_DURATION { return 2; }
33
34 my %RECORD_FROM_COPY_CACHE;
35
36
37 # for security, this is a process-defined and not
38 # a client-defined variable
39 my $__isrenewal = 0;
40
41 # ------------------------------------------------------------------------------
42 # Load the circ script from the config
43 # ------------------------------------------------------------------------------
44 sub initialize {
45
46         my $self = shift;
47         $cache_handle = OpenSRF::Utils::Cache->new('global');
48         my $conf = OpenSRF::Utils::SettingsClient->new;
49         my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
50         my @pfx = ( @pfx2, "scripts" );
51
52         my $p           = $conf->config_value(  @pfx, 'circ_permit_patron' );
53         my $c           = $conf->config_value(  @pfx, 'circ_permit_copy' );
54         my $d           = $conf->config_value(  @pfx, 'circ_duration' );
55         my $f           = $conf->config_value(  @pfx, 'circ_recurring_fines' );
56         my $m           = $conf->config_value(  @pfx, 'circ_max_fines' );
57         my $pr  = $conf->config_value(  @pfx, 'circ_permit_renew' );
58         my $lb  = $conf->config_value(  @pfx2, 'script_path' );
59
60         $logger->error( "Missing circ script(s)" ) 
61                 unless( $p and $c and $d and $f and $m and $pr );
62
63         $scripts{circ_permit_patron}    = $p;
64         $scripts{circ_permit_copy}              = $c;
65         $scripts{circ_duration}                 = $d;
66         $scripts{circ_recurring_fines}= $f;
67         $scripts{circ_max_fines}                = $m;
68         $scripts{circ_permit_renew}     = $pr;
69
70         $lb = [ $lb ] unless ref($lb);
71         $script_libs = $lb;
72
73         $logger->debug("Loaded rules scripts for circ: " .
74                 "circ permit patron: $p, circ permit copy: $c, ".
75                 "circ duration :$d , circ recurring fines : $f, " .
76                 "circ max fines : $m, circ renew permit : $pr");
77 }
78
79
80 # ------------------------------------------------------------------------------
81 # Loads the necessary circ objects and pushes them into the script environment
82 # Returns ( $data, $evt ).  if $evt is defined, then an
83 # unexpedted event occurred and should be dealt with / returned to the caller
84 # ------------------------------------------------------------------------------
85 sub create_circ_ctx {
86         my %params = @_;
87         $U->logmark;
88
89         my $evt;
90         my $ctx = \%params;
91
92         $evt = _ctx_add_patron_objects($ctx, %params);
93         return (undef,$evt) if $evt;
94
95         if(!$params{noncat}) {
96                 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
97                         $ctx->{precat} = 1 if($evt->{textcode} eq 'COPY_NOT_FOUND')
98                 } else {
99                         $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
100                 }
101         }
102
103         _doctor_patron_object($ctx) if $ctx->{patron};
104         _doctor_copy_object($ctx) if $ctx->{copy};
105
106         if(!$ctx->{no_runner}) {
107                 _build_circ_script_runner($ctx);
108                 _add_script_runner_methods($ctx);
109         }
110
111         return $ctx;
112 }
113
114 sub _ctx_add_patron_objects {
115         my( $ctx, %params) = @_;
116         $U->logmark;
117
118         # - patron standings are now handled in the penalty server...
119
120         #if(!defined($cache{patron_standings})) {
121         #       $cache{patron_standings} = $U->fetch_patron_standings();
122         #}
123         #$ctx->{patron_standings} = $cache{patron_standings};
124
125         $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
126         $ctx->{group_tree} = $cache{group_tree};
127
128         $ctx->{patron_circ_summary} = 
129                 $U->fetch_patron_circ_summary($ctx->{patron}->id) 
130                 if $params{fetch_patron_circsummary};
131
132         return undef;
133 }
134
135
136 sub _find_copy_by_attr {
137         my %params = @_;
138         $U->logmark;
139         my $evt;
140
141         my $copy = $params{copy} || undef;
142
143         if(!$copy) {
144
145                 ( $copy, $evt ) = 
146                         $U->fetch_copy($params{copyid}) if $params{copyid};
147                 return (undef,$evt) if $evt;
148
149                 if(!$copy) {
150                         ( $copy, $evt ) = 
151                                 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
152                         return (undef,$evt) if $evt;
153                 }
154         }
155         return ( $copy, $evt );
156 }
157
158 sub _ctx_add_copy_objects {
159         my($ctx, %params)  = @_;
160         $U->logmark;
161         my $evt;
162         my $copy;
163
164         $cache{copy_statuses} = $U->fetch_copy_statuses 
165                 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
166
167         $cache{copy_locations} = $U->fetch_copy_locations 
168                 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
169
170         $ctx->{copy_statuses} = $cache{copy_statuses};
171         $ctx->{copy_locations} = $cache{copy_locations};
172
173         ($copy, $evt) = _find_copy_by_attr(%params);
174         return $evt if $evt;
175
176         if( $copy and !$ctx->{title} ) {
177                 $logger->debug("Copy status: " . $copy->status);
178
179                 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
180                 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
181                 return $evt if $evt;
182                 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
183
184                 $ctx->{title} = $r;
185                 $ctx->{copy} = $copy;
186         }
187
188         return undef;
189 }
190
191
192 # ------------------------------------------------------------------------------
193 # Fleshes parts of the patron object
194 # ------------------------------------------------------------------------------
195 sub _doctor_copy_object {
196         my $ctx = shift;
197         $U->logmark;
198         my $copy = $ctx->{copy} || return undef;
199
200         $logger->debug("Doctoring copy object...");
201
202         # set the copy status to a status name
203         $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
204
205         # set the copy location to the location object
206         $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
207
208         $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
209 }
210
211
212 # ------------------------------------------------------------------------------
213 # Fleshes parts of the patron object
214 # ------------------------------------------------------------------------------
215 sub _doctor_patron_object {
216         my $ctx = shift;
217         $U->logmark;
218         my $patron = $ctx->{patron} || return undef;
219
220         # push the standing object into the patron
221 #       if(ref($ctx->{patron_standings})) {
222 #               for my $s (@{$ctx->{patron_standings}}) {
223 #                       if( $s->id eq $ctx->{patron}->standing ) {
224 #                               $patron->standing($s);
225 #                               $logger->debug("Set patron standing to ". $s->value);
226 #                       }
227 #               }
228 #       }
229
230         # set the patron ptofile to the profile name
231         $patron->profile( _get_patron_profile( 
232                 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
233
234         # flesh the org unit
235         $patron->home_ou( 
236                 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
237
238 }
239
240 # recurse and find the patron profile name from the tree
241 # another option would be to grab the groups for the patron
242 # and cycle through those until the "profile" group has been found
243 sub _get_patron_profile { 
244         my( $patron, $group_tree ) = @_;
245         return $group_tree if ($group_tree->id eq $patron->profile);
246         return undef unless ($group_tree->children);
247
248         for my $child (@{$group_tree->children}) {
249                 my $ret = _get_patron_profile( $patron, $child );
250                 return $ret if $ret;
251         }
252         return undef;
253 }
254
255 sub _get_copy_status {
256         my( $copy, $cstatus ) = @_;
257         $U->logmark;
258         my $s = undef;
259         for my $status (@$cstatus) {
260                 $s = $status if( $status->id eq $copy->status ) 
261         }
262         $logger->debug("Retrieving copy status: " . $s->name) if $s;
263         return $s;
264 }
265
266 sub _get_copy_location {
267         my( $copy, $locations ) = @_;
268         $U->logmark;
269         my $l = undef;
270         for my $loc (@$locations) {
271                 $l = $loc if $loc->id eq $copy->location;
272         }
273         $logger->debug("Retrieving copy location: " . $l->name ) if $l;
274         return $l;
275 }
276
277
278 # ------------------------------------------------------------------------------
279 # Constructs and shoves data into the script environment
280 # ------------------------------------------------------------------------------
281 sub _build_circ_script_runner {
282         my $ctx = shift;
283         $U->logmark;
284
285         $logger->debug("Loading script environment for circulation");
286
287         my $runner;
288         if( $runner = $contexts{$ctx->{type}} ) {
289                 $runner->refresh_context;
290         } else {
291                 $runner = OpenILS::Utils::ScriptRunner->new;
292                 $contexts{type} = $runner;
293         }
294
295         for(@$script_libs) {
296                 $logger->debug("Loading circ script lib path $_");
297                 $runner->add_path( $_ );
298         }
299
300         # Note: inserting the number 0 into the script turns into the
301         # string "0", and thus evaluates to true in JS land
302         # inserting undef will insert "", which evaluates to false
303
304         $runner->insert( 'environment.patron',  $ctx->{patron}, 1);
305         $runner->insert( 'environment.title',   $ctx->{title}, 1);
306         $runner->insert( 'environment.copy',    $ctx->{copy}, 1);
307
308         # circ script result
309         $runner->insert( 'result', {} );
310         #$runner->insert( 'result.event', 'SUCCESS' );
311         $runner->insert( 'result.events', [] );
312
313         if($__isrenewal) {
314                 $runner->insert('environment.isRenewal', 1);
315         } else {
316                 $runner->insert('environment.isRenewal', undef);
317         }
318
319         if($ctx->{ishold} ) { 
320                 $runner->insert('environment.isHold', 1); 
321         } else{ 
322                 $runner->insert('environment.isHold', undef) 
323         }
324
325         if( $ctx->{noncat} ) {
326                 $runner->insert('environment.isNonCat', 1);
327                 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
328         } else {
329                 $runner->insert('environment.isNonCat', undef);
330         }
331
332 #       if(ref($ctx->{patron_circ_summary})) {
333 #               $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
334 #               $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
335 #       }
336
337         $ctx->{runner} = $runner;
338         return $runner;
339 }
340
341
342 sub _add_script_runner_methods {
343         my $ctx = shift;
344         $U->logmark;
345         my $runner = $ctx->{runner};
346
347         if( $ctx->{copy} ) {
348                 
349                 # allows a script to fetch a hold that is currently targeting the
350                 # copy in question
351                 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
352                                 my $key = shift;
353                                 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
354                                 $hold = undef unless $hold;
355                                 $runner->insert( $key, $hold, 1 );
356                         }
357                 );
358         }
359 }
360
361 # ------------------------------------------------------------------------------
362
363 __PACKAGE__->register_method(
364         method  => "permit_circ",
365         api_name        => "open-ils.circ.checkout.permit",
366         notes           => q/
367                 Determines if the given checkout can occur
368                 @param authtoken The login session key
369                 @param params A trailing hash of named params including 
370                         barcode : The copy barcode, 
371                         patron : The patron the checkout is occurring for, 
372                         renew : true or false - whether or not this is a renewal
373                 @return The event that occurred during the permit check.  
374         /);
375
376 __PACKAGE__->register_method (
377         method          => 'permit_circ',
378         api_name                => 'open-ils.circ.checkout.permit.override',
379         signature       => q/@see open-ils.circ.checkout.permit/,
380 );
381
382 sub permit_circ {
383         my( $self, $client, $authtoken, $params ) = @_;
384         $U->logmark;
385
386         my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
387
388         my ( $requestor, $patron, $ctx, $evt, $circ );
389
390         # check permisson of the requestor
391         ( $requestor, $patron, $evt ) = 
392                 $U->checkses_requestor( 
393                 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
394         return $evt if $evt;
395
396         # fetch and build the circulation environment
397         if( !( $ctx = $params->{_ctx}) ) {
398
399                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
400                         patron                                                  => $patron, 
401                         requestor                                               => $requestor, 
402                         type                                                            => 'circ',
403                         #fetch_patron_circ_summary      => 1,
404                         fetch_copy_statuses                     => 1, 
405                         fetch_copy_locations                    => 1, 
406                         );
407                 return $evt if $evt;
408         }
409
410         $ctx->{authtoken} = $authtoken;
411
412         $evt = undef;
413         if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
414                 return $evt unless $U->event_equals($evt, 'SUCCESS');
415         }
416
417         if($evt) { 
418                 $evt = undef;
419
420         } else { 
421
422                 # no claims returned circ was found, check if there is any open circ
423                 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
424                         ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
425                         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
426                 }
427         }
428
429
430         $ctx->{permit_key} = _cache_permit_key();
431         my $events = _run_permit_scripts($ctx);
432
433         if( $override ) {
434                 $evt = override_events($requestor, $requestor->ws_ou, $events);
435                 return $evt if $evt;
436                 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
437         }
438
439         return $events;
440 }
441
442 sub override_events {
443
444         my( $requestor, $org, $events ) = @_;
445         $events = [ $events ] unless ref($events) eq 'ARRAY';
446         my @failed;
447
448         for my $e (@$events) {
449                 my $tc = $e->{textcode};
450                 next if $tc eq 'SUCCESS';
451                 my $ov = "$tc.override";
452                 $logger->info("attempting to override event $ov");
453                 my $evt = $U->check_perms( $requestor->id, $org, $ov );
454                 return $evt if $evt;
455         }
456
457         return undef;
458 }
459
460
461 __PACKAGE__->register_method(
462         method  => "check_title_hold",
463         api_name        => "open-ils.circ.title_hold.is_possible",
464         notes           => q/
465                 Determines if a hold were to be placed by a given user,
466                 whether or not said hold would have any potential copies
467                 to fulfill it.
468                 @param authtoken The login session key
469                 @param params A hash of named params including:
470                         patronid  - the id of the hold recipient
471                         titleid (brn) - the id of the title to be held
472                         depth   - the hold range depth (defaults to 0)
473         /);
474
475 sub check_title_hold {
476         my( $self, $client, $authtoken, $params ) = @_;
477         my %params = %$params;
478         my $titleid = $params{titleid};
479
480         my ( $requestor, $patron, $evt ) = $U->checkses_requestor( 
481                 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
482         return $evt if $evt;
483
484         my $rangelib    = $patron->home_ou;
485         my $depth               = $params{depth} || 0;
486
487         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
488
489         my $org = $U->simplereq(
490                 'open-ils.actor', 
491                 'open-ils.actor.org_unit.retrieve', 
492                 $authtoken, $requestor->home_ou );
493
494         my $limit       = 10;
495         my $offset      = 0;
496         my $title;
497
498         while( $title = $U->storagereq(
499                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
500                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
501
502                 last unless ref($title);
503
504                 for my $cn (@{$title->call_numbers}) {
505         
506                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
507         
508                         for my $copy (@{$cn->copies}) {
509         
510                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
511         
512                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
513                                         {       patron                          => $patron, 
514                                                 requestor                       => $requestor, 
515                                                 copy                                    => $copy,
516                                                 title                                   => $title, 
517                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
518                                                 request_lib                     => $org } );
519         
520                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
521                         }
522                 }
523
524                 $offset += $limit;
525         }
526
527         return 0;
528 }
529
530
531 # Runs the patron and copy permit scripts
532 # if this is a non-cat circulation, the copy permit script 
533 # is not run
534 sub _run_permit_scripts {
535
536         my $ctx                 = shift;
537         my $runner              = $ctx->{runner};
538         my $patronid    = $ctx->{patron}->id;
539         my $barcode             = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
540         my $key                 = $ctx->{permit_key};
541
542         my $penalties = $U->update_patron_penalties( 
543                 authtoken => $ctx->{authtoken}, 
544                 patron    => $ctx->{patron} 
545         );
546
547         $penalties = $penalties->{fatal_penalties};
548
549         $logger->info("circ patron penalties user $patronid: @$penalties");
550
551         if( $ctx->{noncat} ) {
552                 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
553                 return OpenILS::Event->new('SUCCESS', payload => $key);
554         }
555
556         if($ctx->{precat}) {
557                 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
558                 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
559         }
560
561         if($ctx->{ishold}) {
562                 $logger->debug("Exiting circ permit early because request is for hold patron permit");
563                 return OpenILS::Event->new('SUCCESS');
564         }
565
566         $runner->load($scripts{circ_permit_copy});
567         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
568
569         # ---------------------------------------------------------------------
570         # Capture all of the copy permit events
571         # ---------------------------------------------------------------------
572         my $copy_events = $runner->retrieve('result.events');
573         $copy_events = [ split(/,/, $copy_events) ]; 
574         $ctx->{circ_permit_copy_events} = $copy_events;
575         $logger->activity("circ_permit_copy for copy ".
576                 "$barcode returned events: @$copy_events") if @$copy_events;
577
578         my @allevents;
579         push( @allevents, OpenILS::Event->new($_)) for @$penalties;
580         push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
581
582         my $ae = _check_copy_alert($ctx->{copy});
583         push( @allevents, $ae ) if $ae;
584
585         return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
586
587         # uniquify the events
588         my %hash = map { ($_->{ilsevent} => $_) } @allevents;
589         @allevents = values %hash;
590
591         for (@allevents) {
592                 $_->{payload} = $ctx->{copy}->status->id
593                         if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
594         }
595
596         return \@allevents;
597 }
598
599 sub _check_copy_alert {
600         my $copy = shift;
601         return OpenILS::Event->new('COPY_ALERT_MESSAGE', 
602                 payload => $copy->alert_message) if $copy->alert_message;
603         return undef;
604 }
605
606 # takes copyid, patronid, and requestor id
607 sub _cache_permit_key {
608         my $key = md5_hex( time() . rand() . "$$" );
609         $logger->debug("Setting circ permit key to $key");
610         $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
611         return $key;
612 }
613
614 sub _check_permit_key {
615         my $key = shift;
616         $logger->debug("Fetching circ permit key $key");
617         my $k = "oils_permit_key_$key";
618         my $one = $cache_handle->get_cache($k);
619         $cache_handle->delete_cache($k);
620         return ($one) ? 1 : 0;
621 }
622
623
624 # ------------------------------------------------------------------------------
625
626 __PACKAGE__->register_method(
627         method  => "checkout",
628         api_name        => "open-ils.circ.checkout",
629         notes => q/
630                 Checks out an item
631                 @param authtoken The login session key
632                 @param params A named hash of params including:
633                         copy                    The copy object
634                         barcode         If no copy is provided, the copy is retrieved via barcode
635                         copyid          If no copy or barcode is provide, the copy id will be use
636                         patron          The patron's id
637                         noncat          True if this is a circulation for a non-cataloted item
638                         noncat_type     The non-cataloged type id
639                         noncat_circ_lib The location for the noncat circ.  
640                         precat          The item has yet to be cataloged
641                         dummy_title The temporary title of the pre-cataloded item
642                         dummy_author The temporary authr of the pre-cataloded item
643                                 Default is the home org of the staff member
644                 @return The SUCCESS event on success, any other event depending on the error
645         /);
646
647 sub checkout {
648         my( $self, $client, $authtoken, $params ) = @_;
649         $U->logmark;
650
651         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
652         my $key = $params->{permit_key};
653
654         # if this is a renewal, then the requestor does not have to
655         # have checkout privelages
656         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
657         ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
658         return $evt if $evt;
659
660         if( $params->{patron} ) {
661                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
662                 return $evt if $evt;
663         } else {
664                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
665                 return $evt if $evt;
666         }
667
668         # set the circ lib to the home org of the requestor if not specified
669         my $circlib = (defined($params->{circ_lib})) ? 
670                 $params->{circ_lib} : $requestor->ws_ou;
671
672
673         # Make sure the caller has a valid permit key or is 
674         # overriding the permit can
675         if( $params->{permit_override} ) {
676                 $evt = $U->check_perms(
677                         $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
678                 return $evt if $evt;
679
680         } else {
681                 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY') 
682                         unless _check_permit_key($key);
683         }
684
685         # if this is a non-cataloged item, check it out and return
686         return _checkout_noncat( 
687                 $key, $requestor, $patron, %$params ) if $params->{noncat};
688
689         # if this item has yet to be cataloged, make sure a dummy copy exists
690         ( $params->{copy}, $evt ) = _make_precat_copy(
691                 $requestor, $circlib, $params ) if $params->{precat};
692         return $evt if $evt;
693
694
695         # fetch and build the circulation environment
696         if( !( $ctx = $params->{_ctx}) ) {
697                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
698                         patron                                                  => $patron, 
699                         requestor                                               => $requestor, 
700                         session                                                 => $U->start_db_session(),
701                         type                                                            => 'circ',
702                         #fetch_patron_circ_summary      => 1,
703                         fetch_copy_statuses                     => 1, 
704                         fetch_copy_locations                    => 1, 
705                         );
706                 return $evt if $evt;
707         }
708         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
709
710         # if the call doesn't know it's not cataloged..
711         if(!$params->{precat}) {
712                 if( $ctx->{copy}->call_number eq '-1' ) {
713                         return OpenILS::Event->new('ITEM_NOT_CATALOGED');
714                 }
715         }
716
717         # this happens in permit.. but we need to check here for 'offline' requests
718         ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
719         return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
720
721         my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
722
723
724         $ctx->{circ_lib} = $circlib;
725
726         $evt = _run_checkout_scripts($ctx);
727         return $evt if $evt;
728
729
730         _build_checkout_circ_object($ctx);
731
732         $evt = _apply_modified_due_date($ctx);
733         return $evt if $evt;
734
735         $evt = _commit_checkout_circ_object($ctx);
736         return $evt if $evt;
737
738         $evt = _update_checkout_copy($ctx);
739         return $evt if $evt;
740
741         my $holds;
742         ($holds, $evt) = _handle_related_holds($ctx);
743         return $evt if $evt;
744
745
746         $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
747         $U->commit_db_session($ctx->{session});
748         my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
749
750         $logger->activity("user ".$requestor->id." successfully checked out item ".
751                 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
752
753
754         # ------------------------------------------------------------------------------
755         # Update the patron penalty info in the DB
756         # ------------------------------------------------------------------------------
757         $U->update_patron_penalties( 
758                 authtoken => $authtoken, 
759                 patron    => $ctx->{patron} ,
760                 background      => 1,
761         );
762
763         return OpenILS::Event->new('SUCCESS', 
764                 payload => { 
765                         copy                                    => $U->unflesh_copy($ctx->{copy}),
766                         circ                                    => $ctx->{circ},
767                         record                          => $record,
768                         holds_fulfilled => $holds,
769                 } 
770         )
771 }
772
773
774 sub _make_precat_copy {
775         my ( $requestor, $circlib, $params ) =  @_;
776         $U->logmark;
777         my( $copy, undef ) = _find_copy_by_attr(%$params);
778
779         if($copy) {
780                 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
781
782                 $copy->editor($requestor->id);
783                 $copy->edit_date('now');
784                 $copy->dummy_title($params->{dummy_title});
785                 $copy->dummy_author($params->{dummy_author});
786
787                 my $stat = $U->storagereq(
788                         'open-ils.storage.direct.asset.copy.update', $copy );
789
790                 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
791                 return ($copy);
792         }
793
794         $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
795
796         my $evt = OpenILS::Event->new(
797                 'BAD_PARAMS', desc => "Dummy title or author not provided" ) 
798                 unless ( $params->{dummy_title} and $params->{dummy_author} );
799         return (undef, $evt) if $evt;
800
801         $copy = Fieldmapper::asset::copy->new;
802         $copy->circ_lib($circlib);
803         $copy->creator($requestor->id);
804         $copy->editor($requestor->id);
805         $copy->barcode($params->{barcode});
806         $copy->call_number(-1); #special CN for precat materials
807         $copy->loan_duration(&PRECAT_LOAN_DURATION); 
808         $copy->fine_level(&PRECAT_FINE_LEVEL);
809
810         $copy->dummy_title($params->{dummy_title});
811         $copy->dummy_author($params->{dummy_author});
812
813         my $id = $U->storagereq(
814                 'open-ils.storage.direct.asset.copy.create', $copy );
815         return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
816
817         $logger->debug("Pre-cataloged copy successfully created");
818         return ($U->fetch_copy($id));
819 }
820
821
822 sub _run_checkout_scripts {
823         my $ctx = shift;
824         $U->logmark;
825         my $evt;
826         my $circ;
827
828         my $runner = $ctx->{runner};
829
830         $runner->insert('result.durationLevel');
831         $runner->insert('result.durationRule');
832         $runner->insert('result.recurringFinesRule');
833         $runner->insert('result.recurringFinesLevel');
834         $runner->insert('result.maxFine');
835
836         $runner->load($scripts{circ_duration});
837         $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
838         my $duration = $runner->retrieve('result.durationRule');
839         $logger->debug("Circ duration script yielded a duration rule of: $duration");
840
841         $runner->load($scripts{circ_recurring_fines});
842         $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
843         my $recurring = $runner->retrieve('result.recurringFinesRule');
844         $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
845
846         $runner->load($scripts{circ_max_fines});
847         $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
848         my $max_fine = $runner->retrieve('result.maxFine');
849         $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
850
851         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
852         return $evt if $evt;
853         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
854         return $evt if $evt;
855         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
856         return $evt if $evt;
857
858         $ctx->{duration_level}                  = $runner->retrieve('result.durationLevel');
859         $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
860         $ctx->{duration_rule}                   = $duration;
861         $ctx->{recurring_fines_rule}    = $recurring;
862         $ctx->{max_fine_rule}                   = $max_fine;
863
864         return undef;
865 }
866
867 sub _build_checkout_circ_object {
868         my $ctx = shift;
869         $U->logmark;
870
871         my $circ                        = new Fieldmapper::action::circulation;
872         my $duration    = $ctx->{duration_rule};
873         my $max                 = $ctx->{max_fine_rule};
874         my $recurring   = $ctx->{recurring_fines_rule};
875         my $copy                        = $ctx->{copy};
876         my $patron              = $ctx->{patron};
877         my $dur_level   = $ctx->{duration_level};
878         my $rec_level   = $ctx->{recurring_fines_level};
879
880         $circ->duration( $duration->shrt ) if ($dur_level == 1);
881         $circ->duration( $duration->normal ) if ($dur_level == 2);
882         $circ->duration( $duration->extended ) if ($dur_level == 3);
883
884         $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
885         $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
886         $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
887
888         $circ->duration_rule( $duration->name );
889         $circ->recuring_fine_rule( $recurring->name );
890         $circ->max_fine_rule( $max->name );
891         $circ->max_fine( $max->amount );
892
893         $circ->fine_interval($recurring->recurance_interval);
894         $circ->renewal_remaining( $duration->max_renewals );
895         $circ->target_copy( $copy->id );
896         $circ->usr( $patron->id );
897         $circ->circ_lib( $ctx->{circ_lib} );
898
899         if( $__isrenewal ) {
900                 $logger->debug("Circ is a renewal.  Setting renewal_remaining to " . $ctx->{renewal_remaining} );
901                 $circ->opac_renewal(1); 
902                 $circ->renewal_remaining($ctx->{renewal_remaining});
903                 $circ->circ_staff($ctx->{requestor}->id);
904         } 
905
906
907         # if the user provided an overiding checkout time, 
908         # (e.g. the checkout really happened several hours ago), then
909         # we apply that here.  Does this need a perm??
910         if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
911                 $logger->debug("circ setting checkout_time to $ds");
912                 $circ->xact_start($ds);
913         }
914
915         # if a patron is renewing, 'requestor' will be the patron
916         $circ->circ_staff($ctx->{requestor}->id ); 
917         _set_circ_due_date($circ);
918         $ctx->{circ} = $circ;
919 }
920
921 sub _apply_modified_due_date {
922         my $ctx = shift;
923         my $circ = $ctx->{circ};
924
925         if( $ctx->{due_date} ) {
926
927                 my $evt = $U->check_perms(
928                         $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
929                 return $evt if $evt;
930
931                 my $ds = _create_date_stamp($ctx->{due_date});
932                 $logger->debug("circ modifying  due_date to $ds");
933                 $circ->due_date($ds);
934
935         }
936         return undef;
937 }
938
939 sub _create_date_stamp {
940         my $datestring = shift;
941         return undef unless $datestring;
942         $datestring = clense_ISO8601($datestring);
943         $logger->debug("circ created date stamp => $datestring");
944         return $datestring;
945 }
946
947 sub _create_due_date {
948         my $duration = shift;
949         $U->logmark;
950         my ($sec,$min,$hour,$mday,$mon,$year) = 
951                 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
952         $year += 1900; $mon += 1;
953         my $due_date = sprintf(
954         '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
955         $year, $mon, $mday, $hour, $min, $sec);
956         return $due_date;
957 }
958
959 sub _set_circ_due_date {
960         my $circ = shift;
961         $U->logmark;
962         my $dd = _create_due_date($circ->duration);
963         $logger->debug("Checkout setting due date on circ to: $dd");
964         $circ->due_date($dd);
965 }
966
967 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
968 sub _update_checkout_copy {
969         my $ctx = shift;
970         $U->logmark;
971         my $copy = $ctx->{copy};
972
973         my $s = $U->copy_status_from_name('checked out');
974         $copy->status( $s->id ) if $s;
975
976         my $evt = $U->update_copy( session => $ctx->{session}, 
977                 copy => $copy, editor => $ctx->{requestor}->id );
978         return (undef,$evt) if $evt;
979
980         return undef;
981 }
982
983 # commits the circ object to the db then fleshes the circ with rules objects
984 sub _commit_checkout_circ_object {
985
986         my $ctx = shift;
987         my $circ = $ctx->{circ};
988         $U->logmark;
989
990         $circ->clear_id;
991         my $r = $ctx->{session}->request(
992                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
993
994         return $U->DB_UPDATE_FAILED($circ) unless $r;
995
996         $logger->debug("Created a new circ object in checkout: $r");
997
998         $circ->id($r);
999         $circ->duration_rule($ctx->{duration_rule});
1000         $circ->max_fine_rule($ctx->{max_fine_rule});
1001         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1002
1003         return undef;
1004 }
1005
1006
1007 # sees if there are any holds that this copy 
1008 sub _handle_related_holds {
1009
1010         my $ctx         = shift;
1011         my $copy                = $ctx->{copy};
1012         my $patron      = $ctx->{patron};
1013         my $holds       = $holdcode->fetch_related_holds($copy->id);
1014         $U->logmark;
1015         my @fulfilled;
1016
1017         # XXX We should only fulfill one hold here...
1018         # XXX If a hold was transited to the user who is checking out
1019         # the item, we need to make sure that hold is what's grabbed
1020         if(ref($holds) && @$holds) {
1021
1022                 # for now, just sort by id to get what should be the oldest hold
1023                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1024                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1025
1026                 if(@$holds) {
1027                         my $hold = $holds->[0];
1028
1029                         $logger->debug("Related hold found in checkout: " . $hold->id );
1030
1031                         $hold->current_copy($copy->id); # just make sure it's set
1032                         # if the hold was never officially captured, capture it.
1033                         $hold->capture_time('now') unless $hold->capture_time;
1034                         $hold->fulfillment_time('now');
1035                         my $r = $ctx->{session}->request(
1036                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1037                         return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1038                         push( @fulfilled, $hold->id );
1039                 }
1040         }
1041
1042         return (\@fulfilled, undef);
1043 }
1044
1045 sub _checkout_noncat {
1046         my ( $key, $requestor, $patron, %params ) = @_;
1047         my( $circ, $circlib, $evt );
1048         $U->logmark;
1049
1050         $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1051
1052         my $count = $params{noncat_count} || 1;
1053         my $cotime = _create_date_stamp($params{checkout_time}) || "";
1054         $logger->info("circ creating $count noncat circs with checkout time $cotime");
1055         for(1..$count) {
1056                 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1057                         $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1058                 return $evt if $evt;
1059         }
1060
1061         return OpenILS::Event->new( 
1062                 'SUCCESS', payload => { noncat_circ => $circ } );
1063 }
1064
1065
1066 __PACKAGE__->register_method(
1067         method  => "generic_receive",
1068         api_name        => "open-ils.circ.checkin",
1069         argc            => 2,
1070         signature       => q/
1071                 Generic super-method for handling all copies
1072                 @param authtoken The login session key
1073                 @param params Hash of named parameters including:
1074                         barcode - The copy barcode
1075                         force           - If true, copies in bad statuses will be checked in and give good statuses
1076                         ...
1077         /
1078 );
1079
1080 __PACKAGE__->register_method(
1081         method  => "generic_receive",
1082         api_name        => "open-ils.circ.checkin.override",
1083         signature       => q/@see open-ils.circ.checkin/
1084 );
1085
1086 sub generic_receive {
1087         my( $self, $conn, $authtoken, $params ) = @_;
1088         my( $ctx, $requestor, $evt );
1089
1090         ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1091         ( $requestor, $evt ) = $U->checksesperm( 
1092                 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1093         return $evt if $evt;
1094
1095         # load up the circ objects
1096         if( !( $ctx = $params->{_ctx}) ) {
1097                 ( $ctx, $evt ) = create_circ_ctx( %$params, 
1098                         requestor                                               => $requestor, 
1099                         session                                                 => $U->start_db_session(),
1100                         type                                                            => 'circ',
1101                         fetch_copy_statuses                     => 1, 
1102                         fetch_copy_locations                    => 1, 
1103                         no_runner                                               => 1,  
1104                         );
1105                 return $evt if $evt;
1106         }
1107         $ctx->{override} = 1 if $self->api_name =~ /override/o;
1108         $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1109         $ctx->{authtoken} = $authtoken;
1110         my $session = $ctx->{session};
1111
1112         my $copy = $ctx->{copy};
1113         $U->unflesh_copy($copy);
1114         return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1115
1116         $logger->info("Checkin copy called by user ".
1117                 $requestor->id." for copy ".$copy->id);
1118
1119         # ------------------------------------------------------------------------------
1120         # Update the patron penalty info in the DB
1121         # ------------------------------------------------------------------------------
1122         $U->update_patron_penalties( 
1123                 authtoken => $authtoken, 
1124                 patron    => $ctx->{patron},
1125                 background => 1
1126         );
1127
1128         return $self->checkin_do_receive($conn, $ctx);
1129 }
1130
1131 sub checkin_do_receive {
1132
1133         my( $self, $connection, $ctx ) = @_;
1134
1135         my $evt;
1136         my $copy                        = $ctx->{copy};
1137         my $session             = $ctx->{session};
1138         my $requestor   = $ctx->{requestor};
1139         my $change              = 0; # did we actually do anything?
1140         my $circ;
1141
1142         my @eventlist;
1143
1144         # does the copy have an attached alert message?
1145         my $ae = _check_copy_alert($copy);
1146         push(@eventlist, $ae) if $ae;
1147
1148         # is the copy is an a status we can't automatically resolve?
1149         $evt = _checkin_check_copy_status($ctx);
1150         push( @eventlist, $evt ) if $evt;
1151
1152
1153         # - see if the copy has an open circ attached
1154         ($ctx->{circ}, $evt)    = $U->fetch_open_circulation($copy->id);
1155         return $evt if ($evt and $__isrenewal); # renewals require a circulation
1156         $evt = undef;
1157         $circ = $ctx->{circ};
1158
1159         # if the circ is marked as 'claims returned', add the event to the list
1160         push( @eventlist, 'CIRC_CLAIMS_RETURNED' ) 
1161                 if ($circ and $circ->stop_fines eq 'CLAIMSRETURNED');
1162
1163         # override or die
1164         if(@eventlist) {
1165                 if($ctx->{override}) {
1166                         $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1167                         return $evt if $evt;
1168                 } else {
1169                         return \@eventlist;
1170                 }
1171         }
1172
1173         ($ctx->{transit})       = $U->fetch_open_transit_by_copy($copy->id);
1174
1175         if( $ctx->{circ} ) {
1176
1177                 # There is an open circ on this item, close it out.
1178                 $change = 1;
1179                 $evt            = _checkin_handle_circ($ctx);
1180                 return $evt if $evt;
1181
1182         } elsif( $ctx->{transit} ) {
1183
1184                 # is this item currently in transit?
1185                 $change                 = 1;
1186                 $evt                            = $transcode->transit_receive( $copy, $requestor, $session );
1187                 my $holdtrans   = $evt->{holdtransit};
1188                 ($ctx->{hold})  = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1189
1190                 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1191
1192                         # either an error occurred or a ROUTE_ITEM was generated and the 
1193                         # item must be forwarded on to its destination.
1194                         return _checkin_flesh_event($ctx, $evt);
1195
1196                 } else {
1197
1198                         # Transit has been closed, now let's see if the copy's original
1199                         # status is something the staff should be warned of
1200                         my $e = _checkin_check_copy_status($ctx);
1201                         $evt = $e if $e;
1202
1203                         if($holdtrans) {
1204
1205                                 # copy was received as a hold transit.  Copy is at target lib
1206                                 # and hold transit is complete.  We're done here...
1207                                 $U->commit_db_session($session);
1208                                 return _checkin_flesh_event($ctx, $evt);
1209                         }
1210                         $evt = undef;
1211                 }
1212         }
1213
1214         # ------------------------------------------------------------------------------
1215         # Circulations and transits are now closed where necessary.  Now go on to see if
1216         # this copy can fulfill a hold or needs to be routed to a different location
1217         # ------------------------------------------------------------------------------
1218
1219
1220         # If it's a renewal, we're done
1221         if($__isrenewal) {
1222                 $U->commit_db_session($session);
1223                 return OpenILS::Event->new('SUCCESS');
1224         }
1225
1226         # Now, let's see if this copy is needed for a hold
1227         my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor ); 
1228
1229         if($hold) {
1230
1231                 $ctx->{hold}    = $hold;
1232                 $change                 = 1;
1233                 
1234                 # Capture the hold with this copy
1235                 return $evt if ($evt = _checkin_capture_hold($ctx));
1236
1237                 if( $hold->pickup_lib == $requestor->ws_ou ) {
1238
1239                         # This hold was captured in the correct location
1240                         $evt = OpenILS::Event->new('SUCCESS');
1241
1242                 } else {
1243
1244                         # Hold needs to be picked up elsewhere.  Build a hold 
1245                         # transit and route the item.
1246                         return $evt if ($evt =_checkin_build_hold_transit($ctx));
1247                         $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1248                 }
1249
1250         } else { # not needed for a hold
1251
1252                 if( $copy->circ_lib == $requestor->ws_ou ) {
1253
1254                         # Copy is in the right place.
1255                         $evt = OpenILS::Event->new('SUCCESS');
1256
1257                         # if the item happens to be a pre-cataloged item, send it
1258                         # to cataloging and return the event
1259                         my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1260                         return $err if $err;
1261                         $change         = 1 if $c;
1262                         $evt                    = $e if $e;
1263
1264                 } else {
1265
1266                         # Copy wants to go home. Transit it there.
1267                         return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1268                         $evt                    = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1269                         $change         = 1;
1270                 }
1271         }
1272
1273
1274         # ------------------------------------------------------------------
1275         # if the copy is not in a state that should persist,
1276         # set the copy to reshelving if it's not already there
1277         # ------------------------------------------------------------------
1278         my ($c, $e) = _reshelve_copy($ctx);
1279         return $e if $e;
1280         $change = $c unless $change;
1281
1282         if(!$change) {
1283
1284                 $evt = OpenILS::Event->new('NO_CHANGE');
1285                 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id) 
1286                         if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1287
1288         } else {
1289
1290                 $U->commit_db_session($session);
1291         }
1292
1293         $logger->activity("checkin by user ".$requestor->id." on item ".
1294                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1295
1296         return _checkin_flesh_event($ctx, $evt);
1297 }
1298
1299 sub _reshelve_copy {
1300
1301         my $ctx = shift;
1302         my $copy                = $ctx->{copy};
1303         my $reqr                = $ctx->{requestor};
1304         my $session     = $ctx->{session};
1305
1306         my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1307
1308         if($stat != $U->copy_status_from_name('on holds shelf')->id and 
1309                 $stat != $U->copy_status_from_name('available')->id and 
1310                 $stat != $U->copy_status_from_name('cataloging')->id and 
1311                 $stat != $U->copy_status_from_name('in transit')->id and 
1312                 $stat != $U->copy_status_from_name('reshelving')->id ) {
1313
1314                 $copy->status( $U->copy_status_from_name('reshelving')->id );
1315
1316                 my $evt = $U->update_copy( 
1317                         copy            => $copy,
1318                         editor  => $reqr->id,
1319                         session => $session,
1320                         );
1321
1322                 return( 1, $evt );
1323         }
1324         return undef;
1325 }
1326
1327
1328
1329
1330 # returns undef if there are no 'open' claims-returned circs attached
1331 # to the given copy.  if there is an open claims-returned circ, 
1332 # then we check for override mode.  if in override, mark the claims-returned
1333 # circ as checked in.  if not, return event.
1334 sub _handle_claims_returned {
1335         my $ctx = shift;
1336         my $copy = $ctx->{copy};
1337
1338         my $CR  = _fetch_open_claims_returned($copy->id);
1339         return undef unless $CR;
1340
1341         # - If the caller has set the override flag, we will check the item in
1342         if($ctx->{override}) {
1343
1344                 $CR->checkin_time('now');       
1345                 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1346                 $CR->checkin_staff($ctx->{requestor}->id);
1347
1348                 my $stat = $U->storagereq(
1349                         'open-ils.storage.direct.action.circulation.update', $CR);
1350                 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1351                 return OpenILS::Event->new('SUCCESS');
1352
1353         } else {
1354                 # - if not in override mode, return the CR event
1355                 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1356         }
1357 }
1358
1359
1360 sub _fetch_open_claims_returned {
1361         my $copyid = shift;
1362         my $trans = $U->storagereq(
1363                 'open-ils.storage.direct.action.circulation.search_where',
1364                 {       
1365                         target_copy             => $copyid, 
1366                         stop_fines              => 'CLAIMSRETURNED',
1367                         checkin_time    => undef,
1368                 }
1369         );
1370         return $$trans[0] if $trans && $$trans[0];
1371         return undef;
1372 }
1373
1374 # - if the copy is has the 'in process' status, set it to reshelving
1375 #sub _check_in_process {
1376         #my $ctx = shift;
1377 #
1378         #my $copy = $ctx->{copy};
1379         #my $reqr       = $ctx->{requestor};
1380         #my $ses        = $ctx->{session};
1381 ##
1382         #my $stat = $U->copy_status_from_name('in process');
1383         #my $rstat = $U->copy_status_from_name('reshelving');
1384 #
1385         #if( $stat->id == $copy->status->id ) {
1386                 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1387                 #$copy->status( $rstat->id );
1388                 #my $evt = $U->update_copy( 
1389                         #copy           => $copy,
1390                         #editor => $reqr->id,
1391                         #session        => $ses
1392                         #);
1393                 #return $evt if $evt;
1394 #
1395                 #$copy->status( $rstat ); # - reflesh the copy status
1396         #}
1397         #return undef;
1398 #}
1399
1400
1401 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1402 sub _checkin_handle_precat {
1403
1404         my $ctx         = shift;
1405         my $copy                = $ctx->{copy};
1406         my $evt         = undef;
1407         my $errevt      = undef;
1408         my $change      = 0;
1409
1410         my $catstat = $U->copy_status_from_name('cataloging');
1411
1412         if( $ctx->{precat} ) {
1413
1414                 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1415
1416                 if( $copy->status != $catstat->id ) {
1417                         $copy->status($catstat->id);
1418
1419                         return (undef, 0, $errevt) if (
1420                                 $errevt = $U->update_copy(
1421                                         copy            => $copy, 
1422                                         editor  => $ctx->{requestor}->id, 
1423                                         session => $ctx->{session} ));
1424                         $change = 1;
1425
1426                 }
1427         }
1428
1429         return ($evt, $change, undef);
1430 }
1431
1432
1433 # returns the appropriate event for the given copy status
1434 # if the copy is not in a 'special' status, undef is returned
1435 sub _checkin_check_copy_status {
1436         my $ctx = shift;
1437         my $copy = $ctx->{copy};
1438         my $reqr        = $ctx->{requestor};
1439         my $ses = $ctx->{session};
1440
1441         my $islost              = 0;
1442         my $ismissing   = 0;
1443         my $evt                 = undef;
1444
1445         my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1446
1447         return undef 
1448                 if(     $status == $U->copy_status_from_name('available')->id           ||
1449                                 $status == $U->copy_status_from_name('checked out')->id ||
1450                                 $status == $U->copy_status_from_name('in process')->id  ||
1451                                 $status == $U->copy_status_from_name('in transit')->id  ||
1452                                 $status == $U->copy_status_from_name('reshelving')->id );
1453
1454         return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy ) 
1455                 if( $status == $U->copy_status_from_name('lost')->id );
1456
1457         return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy ) 
1458                 if( $status == $U->copy_status_from_name('missing')->id );
1459
1460         return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1461
1462
1463
1464 #       my $rstat = $U->copy_status_from_name('reshelving');
1465 #       my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1466 #
1467 #       if( $stat == $U->copy_status_from_name('lost')->id ) {
1468 #               $islost = 1;
1469 #               $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1470 #
1471 #       } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1472 #               $ismissing = 1;
1473 #               $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1474 #       }
1475 #
1476 #       return (undef,$evt) if(!$ctx->{override});
1477 #
1478 #       # we're are now going to attempt to override the failure 
1479 #       # and set the copy to reshelving
1480 #       my $e;
1481 #       my $copyid = $copy->id;
1482 #       my $userid = $reqr->id;
1483 #       if( $islost ) {
1484 #
1485 #               # - make sure we have permission
1486 #               $e = $U->check_perms( $reqr->id, 
1487 #                       $copy->circ_lib, 'COPY_STATUS_LOST.override');
1488 #               return (undef,$e) if $e;
1489 #               $copy->status( $rstat->id );
1490 #
1491 #               # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1492 #               #my $circ = $U->storagereq(
1493 #               #       'open-ils.storage.direct.action.circulation
1494 #
1495 #               $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1496 #
1497 #       } elsif( $ismissing ) {
1498 #
1499 #               # - make sure we have permission
1500 #               $e = $U->check_perms( $reqr->id, 
1501 #                       $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1502 #               return (undef,$e) if $e;
1503 #               $copy->status( $rstat->id );
1504 #               $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1505 #       }
1506 #
1507 #       if( $islost or $ismissing ) {
1508 #
1509 #               # - update the copy with the new status
1510 #               $evt = $U->update_copy(
1511 #                       copy            => $copy,
1512 #                       editor  => $reqr->id,
1513 #                       session => $ses
1514 #               );
1515 #               return (undef,$evt) if $evt;
1516 #               $copy->status( $rstat );
1517 #       }
1518 #
1519 #       return (1);
1520
1521
1522 }
1523
1524 # Just gets the copy back home.  Returns undef on success, event on error
1525 sub _checkin_build_generic_copy_transit {
1526
1527         my $ctx                 = shift;
1528         my $requestor   = $ctx->{requestor};
1529         my $copy                        = $ctx->{copy};
1530         my $transit             = Fieldmapper::action::transit_copy->new;
1531         my $session             = $ctx->{session};
1532
1533         $logger->activity("User ". $requestor->id ." creating a ".
1534                 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1535
1536         $transit->source($requestor->ws_ou);
1537         $transit->dest($copy->circ_lib);
1538         $transit->target_copy($copy->id);
1539         $transit->source_send_time('now');
1540         $transit->copy_status($copy->status);
1541         
1542         $logger->debug("Creating new copy_transit in DB");
1543
1544         my $s = $session->request(
1545                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1546         return $U->DB_UPDATE_FAILED($transit) unless $s;
1547
1548         $logger->info("Checkin copy successfully created new transit: $s");
1549
1550         $copy->status($U->copy_status_from_name('in transit')->id );
1551
1552         return $U->update_copy( copy => $copy, 
1553                         editor => $requestor->id, session => $session );
1554         
1555 }
1556
1557
1558 # returns event on error, undef on success
1559 sub _checkin_build_hold_transit {
1560         my $ctx = shift;
1561
1562         my $copy = $ctx->{copy};
1563         my $hold = $ctx->{hold};
1564         my $trans = Fieldmapper::action::hold_transit_copy->new;
1565
1566         $trans->hold($hold->id);
1567         $trans->source($ctx->{requestor}->ws_ou);
1568         $trans->dest($hold->pickup_lib);
1569         $trans->source_send_time("now");
1570         $trans->target_copy($copy->id);
1571         $trans->copy_status($copy->status);
1572
1573         my $id = $ctx->{session}->request(
1574                 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1575         return $U->DB_UPDATE_FAILED($trans) unless $id;
1576
1577         $logger->info("Checkin copy successfully created hold transit: $id");
1578
1579         $copy->status($U->copy_status_from_name('in transit')->id );
1580         return $U->update_copy( copy => $copy, 
1581                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1582 }
1583
1584 # Returns event on error, undef on success
1585 sub _checkin_capture_hold {
1586         my $ctx = shift;
1587         my $copy = $ctx->{copy};
1588         my $hold = $ctx->{hold}; 
1589
1590         $logger->debug("Checkin copy capturing hold ".$hold->id);
1591
1592         $hold->current_copy($copy->id);
1593         $hold->capture_time('now'); 
1594
1595         my $stat = $ctx->{session}->request(
1596                 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1597         return $U->DB_UPDATE_FAILED($hold) unless $stat;
1598
1599         $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1600
1601         return $U->update_copy( copy => $copy, 
1602                         editor => $ctx->{requestor}->id, session => $ctx->{session} );
1603 }
1604
1605 # fleshes an event with the relevant objects from the context
1606 sub _checkin_flesh_event {
1607         my $ctx = shift;
1608         my $evt = shift;
1609
1610         my $payload                             = {};
1611         $payload->{copy}                = $U->unflesh_copy($ctx->{copy});
1612         $payload->{record}      = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1613         $payload->{circ}                = $ctx->{circ} if $ctx->{circ};
1614         $payload->{transit}     = $ctx->{transit} if $ctx->{transit};
1615         $payload->{hold}                = $ctx->{hold} if $ctx->{hold};
1616
1617         $evt->{payload} = $payload;
1618         return $evt;
1619 }
1620
1621
1622 # Closes out the circulation, puts the copy into reshelving.
1623 # Voids any bills attached to this circ after the backdate time 
1624 # if a backdate is provided
1625 sub _checkin_handle_circ { 
1626
1627         my $ctx = shift;
1628
1629         my $circ = $ctx->{circ};
1630         my $copy = $ctx->{copy};
1631         my $requestor   = $ctx->{requestor};
1632         my $session             = $ctx->{session};
1633         my $evt;
1634         my $obt;
1635
1636         $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1637
1638         #$ctx->{longoverdue}            = 1 if ($circ->stop_fines =~ /longoverdue/io);
1639         #$ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1640
1641         # backdate the circ if necessary
1642         if(my $backdate = $ctx->{backdate}) {
1643                 return $evt if ($evt = 
1644                         _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1645         }
1646
1647
1648         if(!$circ->stop_fines) {
1649                 $circ->stop_fines('CHECKIN');
1650                 $circ->stop_fines('RENEW') if $__isrenewal;
1651                 $circ->stop_fines_time('now');
1652         }
1653
1654         # see if there are any fines owed on this circ.  if not, close it
1655         ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1656         return $evt if $evt;
1657         $circ->xact_finish('now') if( $obt->balance_owed <= 0 );
1658
1659         # Set the checkin vars since we have the item
1660         $circ->checkin_time('now');
1661         $circ->checkin_staff($requestor->id);
1662         $circ->checkin_lib($requestor->ws_ou);
1663
1664
1665 #       $copy->status($U->copy_status_from_name('reshelving')->id);
1666 #       $evt = $U->update_copy( session => $session, 
1667 #               copy => $copy, editor => $requestor->id );
1668 #       return $evt if $evt;
1669
1670         $ctx->{session}->request(
1671                 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1672
1673         return undef;
1674 }
1675
1676 sub _set_copy_reshelving {
1677         my( $copy, $reqr, $session ) = @_;
1678
1679         $logger->info("Setting copy ".$copy->id." to reshelving");
1680         $copy->status($U->copy_status_from_name('reshelving')->id);
1681
1682         my $evt = $U->update_copy( 
1683                 session => $session, 
1684                 copy            => $copy, 
1685                 editor  => $reqr
1686                 );
1687         return $evt if $evt;
1688 }
1689
1690 # returns event on error, undef on success
1691 # This voids all bills attached to the given circulation that occurred
1692 # after the backdate 
1693 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1694 sub _checkin_handle_backdate {
1695         my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1696
1697         $logger->activity("User ".$requestor->id.
1698                 " backdating circ [".$circ->target_copy."] to date: $backdate");
1699
1700         my $bills = $session->request( # XXX Verify this call is correct
1701                 "open-ils.storage.direct.money.billing.search_where.atomic",
1702                 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1703
1704         if($bills) {
1705                 for my $bill (@$bills) {
1706                         $bill->voided('t');
1707                         my $s = $session->request(
1708                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1709                         return $U->DB_UPDATE_FAILED($bill) unless $s;
1710                 }
1711         }
1712
1713         # if the caller elects to attempt to close the circulation
1714         # transaction, then it will be closed if there are not further
1715         # charges on the transaction
1716         #if( $closecirc ) {
1717                 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1718            #return $evt if $evt;
1719                 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1720         #}
1721
1722         return undef;
1723 }
1724
1725
1726 sub _find_patron_from_params {
1727         my $params = shift;
1728
1729         my $patron;
1730         my $copy;
1731         my $circ;
1732         my $evt;
1733
1734         if(my $barcode = $params->{barcode}) {
1735                 $logger->debug("circ finding user from params with barcode $barcode");
1736                 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1737                 return (undef, undef, $evt) if $evt;
1738                 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1739                 return (undef, undef, $evt) if $evt;
1740                 ($patron, $evt) = $U->fetch_user($circ->usr);
1741                 return (undef, undef, $evt) if $evt;
1742         }
1743         return ($patron, $copy);
1744 }
1745
1746
1747 # ------------------------------------------------------------------------------
1748
1749 __PACKAGE__->register_method(
1750         method  => "renew",
1751         api_name        => "open-ils.circ.renew.override",
1752         signature       => q/@see open-ils.circ.renew/,
1753 );
1754
1755
1756 __PACKAGE__->register_method(
1757         method  => "renew",
1758         api_name        => "open-ils.circ.renew",
1759         notes           => <<"  NOTES");
1760         PARAMS( authtoken, circ => circ_id );
1761         open-ils.circ.renew(login_session, circ_object);
1762         Renews the provided circulation.  login_session is the requestor of the
1763         renewal and if the logged in user is not the same as circ->usr, then
1764         the logged in user must have RENEW_CIRC permissions.
1765         NOTES
1766
1767 sub renew {
1768         my( $self, $client, $authtoken, $params ) = @_;
1769         $U->logmark;
1770
1771         my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1772         $__isrenewal = 1;
1773
1774         $params->{override} = 1 if $self->api_name =~ /override/o;
1775
1776         # fetch the patron object one way or another
1777         if( $params->{patron} ) {
1778                 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1779                 if($evt) { $__isrenewal = 0; return $evt; }
1780
1781         } elsif( $params->{patron_barcode} ) {
1782                 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1783                 if($evt) { $__isrenewal = 0; return $evt; }
1784
1785         } else {
1786                 ($patron, $copy, $evt) = _find_patron_from_params($params);
1787                 return $evt if $evt;
1788                 $params->{copy} = $copy;
1789         }
1790
1791         # verify our login session
1792         ($requestor, $evt) = $U->checkses($authtoken);
1793         if($evt) { $__isrenewal = 0; return $evt; }
1794
1795         # make sure we have permission to perform a renewal
1796         if( $requestor->id ne $patron->id ) {
1797                 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1798                 if($evt) { $__isrenewal = 0; return $evt; }
1799         }
1800
1801
1802         # fetch and build the circulation environment
1803         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1804                 patron                                                  => $patron, 
1805                 requestor                                               => $requestor, 
1806                 patron                                                  => $patron, 
1807                 type                                                            => 'circ',
1808                 #fetch_patron_circ_summary      => 1,
1809                 fetch_copy_statuses                     => 1, 
1810                 fetch_copy_locations                    => 1, 
1811                 );
1812         if($evt) { $__isrenewal = 0; return $evt; }
1813         $params->{_ctx} = $ctx;
1814
1815         # make sure they have some renewals left and make sure the circulation exists
1816         ($circ, $evt) = _check_renewal_remaining($ctx);
1817         if($evt) { $__isrenewal = 0; return $evt; }
1818         $ctx->{old_circ} = $circ;
1819         my $renewals = $circ->renewal_remaining - 1;
1820
1821         # run the renew permit script
1822         $evt = _run_renew_scripts($ctx);
1823         if($evt) { $__isrenewal = 0; return $evt; }
1824
1825         # checkin the cop
1826         #$ctx->{patron} = $ctx->{patron}->id;
1827         $evt = $self->generic_receive($client, $authtoken, $ctx );
1828                 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1829
1830         if( !$U->event_equals($evt, 'SUCCESS') ) {
1831                 $__isrenewal = 0; return $evt; 
1832         }
1833
1834         # re-fetch the context since objects have changed in the checkin
1835         ( $ctx, $evt ) = create_circ_ctx( %$params, 
1836                 patron                                                  => $patron, 
1837                 requestor                                               => $requestor, 
1838                 patron                                                  => $patron, 
1839                 type                                                            => 'circ',
1840                 #fetch_patron_circ_summary      => 1,
1841                 fetch_copy_statuses                     => 1, 
1842                 fetch_copy_locations                    => 1, 
1843                 );
1844         if($evt) { $__isrenewal = 0; return $evt; }
1845         $params->{_ctx} = $ctx;
1846         $ctx->{renewal_remaining} = $renewals;
1847
1848         # run the circ permit scripts
1849         if( $ctx->{permit_override} ) {
1850                 $evt = $U->check_perms(
1851                         $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1852                 if($evt) { $__isrenewal = 0; return $evt; }
1853
1854         } else {
1855                 $evt = $self->permit_circ( $client, $authtoken, $params );
1856                 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1857                         #$ctx->{precat} = 1;
1858                         $params->{precat} = 1;
1859
1860                 } else {
1861                         if(!$U->event_equals($evt, 'SUCCESS')) {
1862                                 if($evt) { $__isrenewal = 0; return $evt; }
1863                         }
1864                 }
1865                 $params->{permit_key} = $evt->{payload};
1866         }
1867
1868
1869         # checkout the item again
1870         $params->{patron} = $ctx->{patron}->id;
1871         $evt = $self->checkout($client, $authtoken, $params );
1872
1873         $logger->activity("user ".$requestor->id." renewl of item ".
1874                 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1875
1876         $__isrenewal = 0;
1877         return $evt;
1878 }
1879
1880 sub _check_renewal_remaining {
1881         my $ctx = shift;
1882         $U->logmark;
1883         my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1884         return (undef, $evt) if $evt;
1885         $evt = OpenILS::Event->new(
1886                 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1887         return ($circ, $evt);
1888 }
1889
1890 sub _run_renew_scripts {
1891         my $ctx = shift;
1892         my $runner = $ctx->{runner};
1893         $U->logmark;
1894
1895         $runner->load($scripts{circ_permit_renew});
1896         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1897
1898         my $events = $runner->retrieve('result.events');
1899         $events = [ split(/,/, $events) ]; 
1900         $logger->activity("circ_permit_renew for user ".
1901                 $ctx->{patron}->id." returned events: @$events") if @$events;
1902
1903         my @allevents;
1904         push( @allevents, OpenILS::Event->new($_)) for @$events;
1905         return \@allevents if  @allevents;
1906
1907         return undef;
1908 }
1909
1910         
1911
1912
1913 1;
1914