module now dies with a message if bootstrap_client has not been called
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Rules.pm
1 # ---------------------------------------------------------------
2 # Copyright (C) 2005  Georgia Public Library Service 
3 # Bill Erickson <highfalutin@gmail.com>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
15
16 package OpenILS::Application::Circ::Rules;
17 use base qw/OpenSRF::Application/;
18 use strict; use warnings;
19
20 use OpenSRF::Utils::SettingsClient;
21 use OpenILS::Utils::Fieldmapper;
22 use OpenILS::EX;
23 use OpenSRF::Utils::Logger qw(:level); 
24
25 use Template;
26 use Template::Stash; 
27
28 use Time::HiRes qw(time);
29 use OpenILS::Utils::ModsParser;
30
31 use OpenSRF::Utils;
32 use OpenSRF::EX qw(:try);
33
34 use OpenILS::Application::AppUtils;
35 my $apputils = "OpenILS::Application::AppUtils";
36 use Digest::MD5 qw(md5_hex);
37
38 my $log = "OpenSRF::Utils::Logger";
39
40 # ----------------------------------------------------------------
41 # rules scripts
42 my $circ_script;
43 my $permission_script;
44 my $duration_script;
45 my $recurring_fines_script;
46 my $max_fines_script;
47 my $permit_hold_script;
48 my $permit_renew_script;
49 # ----------------------------------------------------------------
50
51
52 # data used for this circulation transaction
53 my $circ_objects = {};
54
55 # some static data from the database
56 my $copy_statuses;
57 my $patron_standings;
58 my $patron_profiles;
59 my $shelving_locations;
60
61 # template stash
62 my $stash;
63
64 # memcache for caching the circ objects
65 my $cache_handle;
66
67
68 use constant NO_COPY => 100;
69
70 sub initialize {
71
72         my $self = shift;
73         my $conf = OpenSRF::Utils::SettingsClient->new;
74
75         # ----------------------------------------------------------------
76         # set up the rules scripts
77         # ----------------------------------------------------------------
78         $circ_script = $conf->config_value(                                     
79                 "apps", "open-ils.circ","app_settings", "rules", "main");
80
81         $permission_script = $conf->config_value(                       
82                 "apps", "open-ils.circ","app_settings", "rules", "permission");
83
84         $duration_script = $conf->config_value(                 
85                 "apps", "open-ils.circ","app_settings", "rules", "duration");
86
87         $recurring_fines_script = $conf->config_value(  
88                 "apps", "open-ils.circ","app_settings", "rules", "recurring_fines");
89
90         $max_fines_script = $conf->config_value(                        
91                 "apps", "open-ils.circ","app_settings", "rules", "max_fines");
92
93         $permit_hold_script = $conf->config_value(
94                 "apps", "open-ils.circ","app_settings", "rules", "permit_hold");
95
96         $permit_renew_script = $conf->config_value(
97                 "apps", "open-ils.circ","app_settings", "rules", "permit_renew");
98
99         $log->debug("Loaded rules scripts for circ:\n".
100                 "main - $circ_script : permit circ - $permission_script\n".
101                 "duration - $duration_script : recurring - $recurring_fines_script\n".
102                 "max fines - $max_fines_script : permit hold - $permit_hold_script", DEBUG);
103
104
105         $cache_handle = OpenSRF::Utils::Cache->new();
106 }
107
108
109 sub _grab_patron_standings {
110         my $session = shift;
111         if(!$patron_standings) {
112                 my $standing_req = $session->request(
113                         "open-ils.storage.direct.config.standing.retrieve.all.atomic");
114                 $patron_standings = $standing_req->gather(1);
115                 $patron_standings =
116                         { map { (''.$_->id => $_->value) } @$patron_standings };
117         }
118 }
119
120 sub _grab_patron_profiles {
121         my $session = shift;
122         if(!$patron_profiles) {
123                 my $profile_req = $session->request(
124                         "open-ils.storage.direct.permission.grp_tree.retrieve.all.atomic");
125                 $patron_profiles = $profile_req->gather(1);
126                 $patron_profiles =
127                         { map { (''.$_->id => $_->name) } @$patron_profiles };
128         }
129
130 }
131
132 sub _grab_user {
133         my $session = shift;
134         my $patron_id = shift;
135         my $patron_req  = $session->request(
136                 "open-ils.storage.direct.actor.user.retrieve", 
137                 $patron_id );
138         return $patron_req->gather(1);
139 }
140         
141
142 sub _grab_title_by_copy {
143         my $session = shift;
144         my $copyid = shift;
145         my $title_req   = $session->request(
146                 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
147                 $copyid );
148         return $title_req->gather(1);
149 }
150
151 sub _grab_patron_summary {
152         my $session = shift;
153         my $patron_id = shift;
154         my $summary_req = $session->request(
155                 "open-ils.storage.action.circulation.patron_summary",
156                 $patron_id );
157         return $summary_req->gather(1);
158 }
159
160 sub _grab_copy_by_barcode {
161         my($session, $barcode) = @_;
162         warn "Searching for copy with barcode $barcode\n";
163         my $copy_req    = $session->request(
164                 "open-ils.storage.fleshed.asset.copy.search.barcode", $barcode );
165         return $copy_req->gather(1);
166 }
167
168 sub _grab_copy_by_id {
169         my($session, $id) = @_;
170         warn "Searching for copy with id $id\n";
171         my $copy_req    = $session->request(
172                 "open-ils.storage.direct.asset.copy.retrieve", 
173                 $id );
174         my $c = $copy_req->gather(1);
175         if($c) { return _grab_copy_by_barcode($session, $c->barcode); }
176         return undef;
177 }
178
179
180 sub gather_hold_objects {
181         my($session, $hold, $copy, $args) = @_;
182
183         _grab_patron_standings($session);
184         _grab_patron_profiles($session);
185
186
187         # flesh me
188         $copy = _grab_copy_by_barcode($session, $copy->barcode) unless ref($copy->circ_lib);
189
190         my $hold_objects = {};
191         $hold_objects->{standings} = $patron_standings;
192         $hold_objects->{copy}           = $copy;
193         $hold_objects->{hold}           = $hold;
194         $hold_objects->{title}          = $$args{title} || _grab_title_by_copy($session, $copy->id);
195         $hold_objects->{requestor} = $$args{requestor} || _grab_user($session, $hold->requestor);
196         my $patron                                              = $$args{usr} || _grab_user($session, $hold->usr);
197
198         $copy->status( $copy->status->name );
199         $patron->standing($patron_standings->{$patron->standing()});
200         $patron->profile( $patron_profiles->{$patron->profile});
201
202         $hold_objects->{patron}         = $patron;
203
204         return $hold_objects;
205 }
206
207
208
209 __PACKAGE__->register_method(
210         method  => "permit_hold",
211         api_name        => "open-ils.circ.permit_hold",
212         notes           => <<"  NOTES");
213         Determines whether a given copy is eligible to be held
214         NOTES
215
216 sub permit_hold {
217         my( $self, $client, $hold, $copy, $args ) = @_;
218
219         my $session     = OpenSRF::AppSession->create("open-ils.storage");
220         
221         # collect items necessary for circ calculation
222         my $hold_objects = gather_hold_objects( $session, $hold, $copy, $args );
223
224         $stash = Template::Stash->new(
225                         circ_objects                    => $hold_objects,
226                         result                                  => []);
227
228         $stash->set("run_block", $permit_hold_script);
229
230         # grab the number of copies checked out by the patron as
231         # well as the total fines
232         my $summary = _grab_patron_summary($session, $hold_objects->{patron}->id);
233         $summary->[0] ||= 0;
234         $summary->[1] ||= 0.0;
235
236         $stash->set("patron_copies", $summary->[0] );
237         $stash->set("patron_fines", $summary->[1] );
238
239         # run the permissibility script
240         run_script();
241         my $result = $stash->get("result");
242
243         # 0 means OK in the script
244         return 1 if($result->[0] == 0);
245         return 0;
246
247 }
248
249
250
251
252
253 # ----------------------------------------------------------------
254 # Collect all of the objects necessary for calculating the
255 # circ matrix.
256 # ----------------------------------------------------------------
257 sub gather_circ_objects {
258         my( $session, $barcode_string, $patron_id, $copy ) = @_;
259
260         throw OpenSRF::EX::ERROR 
261                 ("gather_circ_objects needs data")
262                         unless ($barcode_string and $patron_id);
263
264         warn "Gathering circ objects with barcode $barcode_string and patron id $patron_id\n";
265
266         # see if all of the circ objects are in cache
267 #       my $cache_key =  "circ_object_" . md5_hex( $barcode_string, $patron_id );
268 #       $circ_objects = $cache_handle->get_cache($cache_key);
269
270 #       if($circ_objects) { 
271 #               $stash = Template::Stash->new(
272 #                       circ_objects                    => $circ_objects,
273 #                       result                                  => [],
274 #                       target_copy_status      => 1,
275 #                       );
276 #               return;
277 #       }
278
279         # only necessary if the circ objects have not been built yet
280
281         _grab_patron_standings($session);
282         _grab_patron_profiles($session);
283
284
285         if(!$copy) {
286                 $copy = _grab_copy_by_barcode($session, $barcode_string);
287                 if(!$copy) { return NO_COPY; }
288         }
289
290         my $patron = _grab_user($session, $patron_id);
291
292         $copy->status( $copy->status->name );
293         $circ_objects->{copy} = $copy;
294
295         $patron->standing($patron_standings->{$patron->standing()});
296         $patron->profile( $patron_profiles->{$patron->profile});
297         $circ_objects->{patron} = $patron;
298         $circ_objects->{standings} = $patron_standings;
299
300         #$circ_objects->{title} = $title_req->gather(1);
301         $circ_objects->{title} = _grab_title_by_copy($session, $circ_objects->{copy}->id);
302 #       $cache_handle->put_cache( $cache_key, $circ_objects, 30 );
303
304         $stash = Template::Stash->new(
305                         circ_objects                    => $circ_objects,
306                         result                                  => [],
307                         target_copy_status      => 1,
308                         );
309 }
310
311
312
313 sub run_script {
314
315         my $result;
316
317         my $template = Template->new(
318                 { 
319                         STASH                   => $stash,
320                         ABSOLUTE                => 1, 
321                         OUTPUT          => \$result,
322                 }
323         );
324
325         my $status = $template->process($circ_script);
326
327         if(!$status) { 
328                 throw OpenSRF::EX::ERROR 
329                         ("Error processing circ script " .  $template->error()); 
330         }
331
332         warn "Script result: $result\n";
333 }
334
335
336
337
338 __PACKAGE__->register_method(
339         method  => "permit_circ",
340         api_name        => "open-ils.circ.permit_checkout",
341 );
342
343 sub permit_circ {
344         my( $self, $client, $user_session, $barcode, $user_id, $outstanding_count ) = @_;
345
346         my $copy_status_mangled;
347         my $hold;
348
349         my $renew = 0;
350         if(defined($outstanding_count) && $outstanding_count eq "renew") {
351                 $renew = 1;
352                 $outstanding_count = 0;
353         } else { $outstanding_count ||= 0; }
354
355         my $session     = OpenSRF::AppSession->create("open-ils.storage");
356         
357         # collect items necessary for circ calculation
358         my $status = gather_circ_objects( $session, $barcode, $user_id );
359
360         if( $status == NO_COPY ) {
361                 return { record => undef, 
362                         status => NO_COPY, 
363                         text => "No copy available with barcode $barcode"
364                 };
365         }
366
367         my $copy = $stash->get("circ_objects")->{copy};
368
369         warn "Found copy in permit: " . $copy->id . "\n";
370
371         warn "Copy status in checkout is " . $stash->get("circ_objects")->{copy}->status . "\n";
372
373         $stash->set("run_block", $permission_script);
374
375         # grab the number of copies checked out by the patron as
376         # well as the total fines
377         my $summary_req = $session->request(
378                 "open-ils.storage.action.circulation.patron_summary",
379                 $stash->get("circ_objects")->{patron}->id );
380
381         my $summary = $summary_req->gather(1);
382
383         $summary->[0] ||= 0;
384         $summary->[1] ||= 0.0;
385
386         $stash->set("patron_copies", $summary->[0]  + $outstanding_count );
387         $stash->set("patron_fines", $summary->[1] );
388         $stash->set("renew", 1) if $renew; 
389
390         # run the permissibility script
391         run_script();
392
393         my $arr = $stash->get("result");
394
395         if( $arr->[0]  eq "0" ) { # and $copy_status_mangled == 8) {
396
397                 # see if this copy is needed to fulfil a hold
398
399                 warn "Searching for hold for checkout of copy " . $copy->id . "\n";
400
401                 my $hold = $session->request(
402                         "open-ils.storage.direct.action.hold_request.search.atomic",
403                          current_copy =>  $copy->id , fulfillment_time => undef )->gather(1);
404
405                 $hold = $hold->[0];
406
407                 if($hold) { warn "Found hold in circ permit with id " . $hold->id . "\n"; }
408
409                 if($hold) {
410
411                         warn "Found unfulfilled hold in permit ". $hold->id . "\n";
412
413                         if( $hold->usr eq $user_id ) {
414                                 return { status => 0, text => "OK" };
415
416                         } else {
417                                 return { status => 6, 
418                                         text => "Copy is needed by a different user to fulfill a hold" };
419                         }
420                 } 
421         }
422         
423         return { status => $arr->[0], text => $arr->[1] };
424
425 }
426
427
428
429 __PACKAGE__->register_method(
430         method  => "circulate",
431         api_name        => "open-ils.circ.checkout.barcode",
432 );
433
434 sub circulate {
435         my( $self, $client, $user_session, $barcode, $patron, $isrenew, $numrenews ) = @_;
436
437
438         my $user = $apputils->check_user_session($user_session);
439         my $session = $apputils->start_db_session();
440
441         my $copy =  _grab_copy_by_barcode($session, $barcode);
442         my $realstatus = $copy->status->id;
443
444         warn "Checkout copy status is $realstatus\n";
445
446         gather_circ_objects( $session, $barcode, $patron, $copy );
447
448         # grab the copy statuses if we don't already have them
449         if(!$copy_statuses) {
450                 my $csreq = $session->request(
451                         "open-ils.storage.direct.config.copy_status.retrieve.all.atomic" );
452                 $copy_statuses = $csreq->gather(1);
453         }
454
455         # put copy statuses into the stash
456         $stash->set("copy_statuses", $copy_statuses );
457
458         $copy = $circ_objects->{copy};
459         my ($circ, $duration, $recurring, $max) =  run_circ_scripts($session);
460
461
462         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = 
463                 gmtime(OpenSRF::Utils->interval_to_seconds($circ->duration) + int(time()));
464         $year += 1900; $mon += 1;
465         my $due_date = sprintf(
466         '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
467         $year, $mon, $mday, $hour, $min, $sec);
468
469         warn "Setting due date to $due_date\n";
470         $circ->due_date($due_date);
471         $circ->circ_lib($user->home_ou);
472
473         if($isrenew) {
474                 warn "Renewing circ.... and setting num renews to " . $numrenews - 1 . "\n";
475                 $circ->opac_renewal(1); # XXX different for different types ! 
476                 $circ->clear_id;
477                 $circ->renewal_remaining($numrenews - 1);
478                 $circ->circ_staff($patron);
479         } else {
480                 $circ->circ_staff($user->id);
481         }
482
483
484         # commit new circ object to db
485         my $commit = $session->request(
486                 "open-ils.storage.direct.action.circulation.create", $circ );
487         my $id = $commit->gather(1);
488
489         if(!$id) {
490                 throw OpenSRF::EX::ERROR 
491                         ("Error creating new circulation object");
492         }
493
494         # update the copy with the new circ
495         $copy->status( $stash->get("target_copy_status") );
496         $copy->location( $copy->location->id );
497         $copy->circ_lib( $copy->circ_lib->id ); #XXX XXX needs to point to the lib that actually checked out the item (user->home_ou)?
498
499         # commit copy to db
500         my $copy_update = $session->request(
501                 "open-ils.storage.direct.asset.copy.update",
502                 $copy );
503         $copy_update->gather(1);
504
505
506         if( $realstatus eq "8" ) { # on holds shelf
507
508
509                 warn "Searching for hold for checkout of copy " . $copy->id . "\n";
510
511                 my $hold = $session->request(
512                         "open-ils.storage.direct.action.hold_request.search.atomic",
513                          current_copy =>  $copy->id , fulfillment_time => undef )->gather(1);
514                 $hold = $hold->[0];
515
516                 if($hold) {
517
518                         $hold->fulfillment_time("now");
519                                 my $s = $session->request(
520                                         "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
521                                 if(!$s) { throw OpenSRF::EX::ERROR ("Error updating hold");}
522                 }
523         }
524
525         $apputils->commit_db_session($session);
526
527         # remove our circ object from the cache
528         $cache_handle->delete_cache("circ_object_" . md5_hex($barcode, $patron));
529
530         # re-retrieve the the committed circ object  
531         $circ = $apputils->simple_scalar_request(
532                 "open-ils.storage",
533                 "open-ils.storage.direct.action.circulation.retrieve",
534                 $id );
535
536
537         # push the rules and due date into the circ object
538         $circ->duration_rule($duration);
539         $circ->max_fine_rule($max);
540         $circ->recuring_fine_rule($recurring);
541
542         # turn the biblio record into a friendly object
543         my $obj = $stash->get("circ_objects");
544         my $u = OpenILS::Utils::ModsParser->new();
545         $u->start_mods_batch( $circ_objects->{title}->marc() );
546         my $mods = $u->finish_mods_batch();
547
548
549         return { circ => $circ, copy => $copy, record => $mods };
550
551 }
552
553
554
555 # runs the duration, recurring_fines, and max_fines scripts.
556 # builds the new circ object based on the rules returned from 
557 # these scripts. 
558 # returns (circ, duration_rule, recurring_fines_rule, max_fines_rule)
559 sub run_circ_scripts {
560         my $session = shift;
561
562         # go through all of the scripts and process
563         # each script returns 
564         # [ rule_name, level (appropriate to the script) ]
565         $stash->set("result", [] );
566         $stash->set("run_block", $duration_script);
567         run_script();
568         my $duration_rule = $stash->get("result");
569
570         $stash->set("run_block", $recurring_fines_script);
571         $stash->set("result", [] );
572         run_script();
573         my $rec_fines_rule = $stash->get("result");
574
575         $stash->set("run_block", $max_fines_script);
576         $stash->set("result", [] );
577         run_script();
578         my $max_fines_rule = $stash->get("result");
579
580         my $obj = $stash->get("circ_objects");
581
582         # ----------------------------------------------------------
583         # find the rules objects based on the rule names returned from
584         # the various scripts.
585         my $dur_req = $session->request(
586                 "open-ils.storage.direct.config.rules.circ_duration.search.name.atomic",
587                 $duration_rule->[0] );
588
589         my $rec_req = $session->request(
590                 "open-ils.storage.direct.config.rules.recuring_fine.search.name.atomic",
591                 $rec_fines_rule->[0] );
592
593         my $max_req = $session->request(
594                 "open-ils.storage.direct.config.rules.max_fine.search.name.atomic",
595                 $max_fines_rule->[0] );
596
597         my $duration    = $dur_req->gather(1)->[0];
598         my $recurring   = $rec_req->gather(1)->[0];
599         my $max                 = $max_req->gather(1)->[0];
600
601         my $copy = $circ_objects->{copy};
602
603         use Data::Dumper;
604         warn "Building a new circulation object with\n".
605                 "=> copy "                              . Dumper($copy) .
606                 "=> duration_rule "     . Dumper($duration_rule) .
607                 "=> rec_files_rule " . Dumper($rec_fines_rule) .
608                 "=> duration "                  . Dumper($duration) .
609                 "=> recurring "         . Dumper($recurring) .
610                 "=> max "                               . Dumper($max);
611
612
613         # build the new circ object
614         my $circ =  build_circ_object($session, $copy, $duration_rule->[1], 
615                         $rec_fines_rule->[1], $duration, $recurring, $max );
616
617         return ($circ, $duration, $recurring, $max);
618
619 }
620
621 # ------------------------------------------------------------------
622 # Builds a new circ object
623 # ------------------------------------------------------------------
624 sub build_circ_object {
625         my( $session, $copy, $dur_level, $rec_level, 
626                         $duration, $recurring, $max ) = @_;
627
628         my $circ = new Fieldmapper::action::circulation;
629
630         $circ->circ_lib( $copy->circ_lib->id() );
631         if($dur_level == 1) {
632                 $circ->duration( $duration->shrt );
633         } elsif($dur_level == 2) {
634                 $circ->duration( $duration->normal );
635         } elsif($dur_level == 3) {
636                 $circ->duration( $duration->extended );
637         }
638
639         if($rec_level == 1) {
640                 $circ->recuring_fine( $recurring->low );
641         } elsif($rec_level == 2) {
642                 $circ->recuring_fine( $recurring->normal );
643         } elsif($rec_level == 3) {
644                 $circ->recuring_fine( $recurring->high );
645         }
646
647         $circ->duration_rule( $duration->name );
648         $circ->recuring_fine_rule( $recurring->name );
649         $circ->max_fine_rule( $max->name );
650         $circ->max_fine( $max->amount );
651
652         $circ->fine_interval($recurring->recurance_interval);
653         $circ->renewal_remaining( $duration->max_renewals );
654         $circ->target_copy( $copy->id );
655         $circ->usr( $circ_objects->{patron}->id );
656
657         return $circ;
658
659 }
660
661 __PACKAGE__->register_method(
662         method  => "transit_receive",
663         api_name        => "open-ils.circ.transit.receive",
664         notes           => <<"  NOTES");
665         Receives a copy that is in transit.  
666         Params are login_session and copyid.
667         Logged in user must have COPY_CHECKIN priveleges.
668
669         status 3 means that this transit is destined for somewhere else
670         status 10 means the copy is not in transit
671         status 11 means the transit is complete, does not need processing
672         status 12 means copy is in transit but no tansit was found
673
674         NOTES
675
676 sub transit_receive {
677         my( $self, $client, $login_session, $copyid ) = @_;
678
679         my $user = $apputils->check_user_session($login_session);
680
681         if($apputils->check_user_perms($user->id, $user->home_ou, "COPY_CHECKIN")) {
682                 return OpenILS::Perm->new("COPY_CHECKIN");
683         }
684
685         warn "Receiving copy for transit $copyid\n";
686
687         my $session = $apputils->start_db_session();
688         my $copy = $session->request(
689                         "open-ils.storage.direct.asset.copy.retrieve", $copyid)->gather(1);
690         my $transit;
691
692         if(!$copy->status eq "6") {
693                 return { status => 10, route_to => $copy->circ_lib };
694         }
695
696
697         $transit = $session->request(
698                 "open-ils.storage.direct.action.transit_copy.search_where",
699                 { target_copy => $copy->id, dest_recv_time => undef } )->gather(1);
700
701
702         my $record = _grab_title_by_copy($session, $copy->id);
703         my $u = OpenILS::Utils::ModsParser->new();
704         $u->start_mods_batch( $record->marc() );
705         $record = $u->finish_mods_batch();
706
707         if($transit) {
708
709                 warn "Found transit " . $transit->id . " for copy $copyid\n";
710
711                 if( defined($transit->dest_recv_time) ) {
712                         return { status => 11, route_to => $copy->circ_lib, 
713                                 text => "Transit is already complete for this copy" };
714                 }
715
716                 $transit->dest_recv_time("now");
717                 my $s = $session->request(
718                         "open-ils.storage.direct.action.transit_copy.update", $transit )->gather(1);
719
720                 if(!$s) { throw OpenSRF::EX::ERROR ("Error updating transit " . $transit->id . "\n"); }
721
722                 warn "Searching for hold transit with id " . $transit->id . "\n";
723
724                 my $holdtransit = $session->request(
725                         "open-ils.storage.direct.action.hold_transit_copy.retrieve",
726                         $transit->id )->gather(1);
727
728                 if($holdtransit) {
729
730                         warn "Found hold transit for copy $copyid\n";
731
732                         my $hold = $session->request(
733                                 "open-ils.storage.direct.action.hold_request.retrieve",
734                                 $holdtransit->hold )->gather(1);
735
736                         if(!$hold) {
737                                 throw OpenSRF::EX::ERROR ("No hold found to match transit " . $holdtransit->id);
738                         }
739
740                         # put copy on the holds shelf
741                         $copy->status(8); #hold shelf status
742                         $copy->editor($user->id); #hold shelf status
743                         $copy->edit_date("now"); #hold shelf status
744
745                         warn "Updating copy " . $copy->id . " with new status, editor, and edit date\n";
746
747                         my $s = $session->request(
748                                 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
749                         if(!$s) {throw OpenSRF::EX::ERROR ("Error putting copy on holds shelf ".$copy->id);} # blah..
750
751                         $apputils->commit_db_session($session);
752
753                         return { status => 4, route_to => "Holds Shelf", 
754                                 text => "Transit Complete", record => $record, copy => $copy  };
755
756
757                 } else {
758
759                         if($transit->dest eq $user->home_ou) {
760
761                                 $copy->status(0); 
762                                 $copy->editor($user->id); 
763                                 $copy->edit_date("now"); 
764
765                                 my $s = $session->request(
766                                         "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
767                                 if(!$s) {throw OpenSRF::EX::ERROR ("Error updating copy ".$copy->id);} # blah..
768
769                                 my($status, $status_text) = (0, "Transit Complete");
770
771                                 my $circ;
772
773                                 if($transit->copy_status eq "3") { #if copy is lost
774                                         $status = 2;
775                                         $status_text = "Copy is marked as LOST";
776
777                                         $circ = $session->request(
778                                                 "open-ils.storage.direct.action.circulation.search_where",
779                                                 { target_copy => $copy->id, xact_finish => undef }, 
780                                                 { order_by => "xact_start desc", limit => 1 } )->gather(1);
781
782
783                                         if($circ) {
784
785                                                 my $transaction = $session->request(
786                                                         "open-ils.storage.direct.money.open_billable_transaction_summary.retrieve", $circ->id)->gather(1);
787
788                                                 $circ->xact_finish("now") if($transaction->balance_owed <= 0);
789
790                                                 my $s = $session->request(
791                                                         "open-ils.storage.direct.action.circulation.update", $circ )->gather(1);
792
793                                                 if(!$s) {
794                                                         throw OpenSRF::EX::ERROR ("Error updating circulation " . $circ->id);
795                                                 }
796                                         }
797
798                                 }
799
800                                 $apputils->commit_db_session($session);
801
802                                 return { 
803                                         status => $status, 
804                                         route_to => $user->home_ou, 
805                                         text => $status_text, 
806                                         record => $record, 
807                                         circ    => $circ,
808                                         copy => $copy  };
809
810                         } else {
811
812                                 $apputils->rollback_db_session($session);
813
814
815                                 return { 
816                                         copy => $copy, record => $record, 
817                                         status => 3, route_to => $transit->dest, 
818                                         text => "Transit needs to be routed" };
819
820                         }
821
822                 }
823
824
825         } else { 
826
827                 $apputils->rollback_db_session($session);
828                 return { status => 12, route_to => $copy->circ_lib, text => "No transit found" };
829         } 
830
831 }
832
833
834
835 __PACKAGE__->register_method(
836         method  => "checkin",
837         api_name        => "open-ils.circ.checkin.barcode",
838         notes           => <<"  NOTES");
839         Checks in based on barcode
840         Returns record, status, text, circ, copy, route_to 
841         'status' values:
842                 0 = OK
843                 1 = 'copy required to fulfil a hold'
844                 2 = "copy is marked as lost"
845                 3 = "transit copy"
846                 4 = "transit for hold complete, put on holds shelf"
847         NOTES
848
849 sub checkin {
850         my( $self, $client, $user_session, $barcode, $isrenewal, $backdate ) = @_;
851
852         my $err;
853         my $copy;
854         my $circ;
855         my $iamlost;
856
857         my $status = "0";
858         my $status_text = "OK";
859         my $route_to = "";
860
861         my $transaction;
862         my $user = $apputils->check_user_session($user_session);
863
864         if($apputils->check_user_perms($user->id, $user->home_ou, "COPY_CHECKIN")) {
865                 return OpenILS::Perm->new("COPY_CHECKIN");
866         }
867
868         my $session = $apputils->start_db_session();
869         my $transit_return;
870
871         my $orig_copy_status;
872
873
874         try {
875                         
876                 warn "retrieving copy for checkin\n";
877
878                         
879                 my $copy_req = $session->request(
880                         "open-ils.storage.direct.asset.copy.search.barcode.atomic", 
881                         $barcode );
882                 $copy = $copy_req->gather(1)->[0];
883                 if(!$copy) {
884                         $client->respond_complete(OpenILS::EX->new("UNKNOWN_BARCODE")->ex);
885                 }
886
887
888                 if($copy->status eq "3") { #if copy is lost
889                         $iamlost = 1;
890                 }
891
892                 if($copy->status eq "6") { #copy is in transit, deal with it
893                         my $method = $self->method_lookup("open-ils.circ.transit.receive");
894                         ($transit_return) = $method->run( $user_session, $copy->id );
895
896                 } else {
897
898
899                         if(!$shelving_locations) {
900                                 my $sh_req = $session->request(
901                                         "open-ils.storage.direct.asset.copy_location.retrieve.all.atomic");
902                                 $shelving_locations = $sh_req->gather(1);
903                                 $shelving_locations = 
904                                         { map { (''.$_->id => $_->name) } @$shelving_locations };
905                         }
906         
907                         
908                         $orig_copy_status = $copy->status;
909                         $copy->status(0);
910                 
911                         # find circ's where the transaction is still open for the
912                         # given copy.  should only be one.
913                         warn "Retrieving circ for checkin\n";
914                         my $circ_req = $session->request(
915                                 "open-ils.storage.direct.action.circulation.search.atomic",
916                                 { target_copy => $copy->id, xact_finish => undef }, 
917                                 { order_by => "xact_start desc", limit => 1 } );
918                 
919                         $circ = $circ_req->gather(1)->[0];
920         
921                 
922                         if(!$circ) {
923                                 $err = "No circulation exists for the given barcode";
924         
925                         } else {
926         
927                                 $transaction = $session->request(
928                                         "open-ils.storage.direct.money.open_billable_transaction_summary.retrieve", $circ->id)->gather(1);
929                 
930                                 warn "Checking in circ ". $circ->id . "\n";
931                         
932                                 $circ->stop_fines("CHECKIN");
933                                 $circ->stop_fines("RENEW") if($isrenewal);
934                                 $circ->stop_fines("LOST") if($iamlost);
935                                 $circ->xact_finish("now") if($transaction->balance_owed <= 0 and !$iamlost);
936                                 $circ->stop_fines_time('now');
937                                 $circ->checkin_time('now');
938                                 $circ->checkin_staff($user->id);
939
940                                 if($backdate) { 
941                                         $circ->xact_finish($backdate); 
942
943                                         # void any bills the resulted after the backdate time
944                                         my $bills = $session->request(
945                                                 "open-ils.storage.direct.money.billing.search_where.atomic",
946                                                 billing_ts => { ">=" => $backdate })->gather(1);
947
948                                         if($bills) {
949                                                 for my $bill (@$bills) {
950
951                                                         $bill->voided('t');
952                                                         my $s = $session->request(
953                                                                 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
954
955                                                         if(!$s) { 
956                                                                 throw OpenSRF::EX::ERROR 
957                                                                         ("Error voiding bill on checkin with backdate : $backdate, circ id: " . $circ->id);
958                                                         }
959                                                 }
960                                         }
961                                 }
962                         
963                                 my $cp_up = $session->request(
964                                         "open-ils.storage.direct.asset.copy.update", $copy );
965                                 $cp_up->gather(1);
966                         
967                                 my $ci_up = $session->request(
968                                         "open-ils.storage.direct.action.circulation.update", $circ );
969
970
971                                 $ci_up->gather(1);
972                         
973                                 warn "Checkin succeeded\n";
974                         }
975                 }
976                 
977         } catch Error with {
978                 my $e = shift;
979                 $err = "Error checking in: $e";
980         };
981         
982         if($transit_return) { return $transit_return; }
983
984         if($err) {
985
986                 return { record => undef, status => -1, text => $err };
987
988         } else {
989
990
991                 # see if this copy can fulfill a hold
992                 my ( $hold, $evt ) = OpenILS::Application::Circ::Holds::_find_local_hold_for_copy( $session, $copy, $user );
993
994                 $route_to = $shelving_locations->{$copy->location};
995
996                 if($hold) { 
997                         warn "We found a hold that can be fulfilled by copy " . $copy->id . "\n";
998                         $status = "1";
999                         $status_text = "Copy needed to fulfill hold";
1000                         $route_to = $hold->pickup_lib;
1001                 }
1002
1003                 if($iamlost) {
1004                         $status = "2";
1005                         $status_text = "Copy is marked as LOST";
1006                 }
1007
1008                 if(!$hold and $copy->circ_lib ne $user->home_ou) {
1009
1010                         warn "Checked in copy needs to be transited " . $copy->id . "\n";
1011
1012                         my $transit = Fieldmapper::action::transit_copy->new;
1013                         $transit->source($user->home_ou);
1014                         $transit->dest($copy->circ_lib);
1015                         $transit->target_copy($copy->id);
1016                         $transit->source_send_time("now");
1017                         $transit->copy_status($orig_copy_status);
1018
1019                         my $s = $session->request(
1020                                 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1021
1022                         if(!$s){ throw OpenSRF::EX::ERROR 
1023                                 ("Unable to create new transit for copy " . $copy->id ); }
1024
1025                         warn "Putting copy into in transit state \n";
1026                         $copy->status(6); 
1027                         $copy->editor($user->id); 
1028                         $copy->edit_date("now"); 
1029
1030                         $s = $session->request(
1031                                 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
1032                         if(!$s) {throw OpenSRF::EX::ERROR ("Error updating copy ".$copy->id);} # blah..
1033
1034                         $status = 3;
1035                         $status_text = "Copy needs to be routed to a different location";
1036                         $route_to = $copy->circ_lib;
1037                 }
1038
1039
1040         
1041                 $apputils->commit_db_session($session);
1042
1043                 my $record = $apputils->simple_scalar_request(
1044                         "open-ils.storage",
1045                         "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
1046                         $copy->id() );
1047
1048                 my $mods = undef;
1049                 if( $record->marc ) {
1050                         my $u = OpenILS::Utils::ModsParser->new();
1051                         $u->start_mods_batch( $record->marc() );
1052                         $mods = $u->finish_mods_batch();
1053                 }
1054
1055                 return { 
1056                         record  => $mods, 
1057                         status  => $status,
1058                         text            => $status_text,
1059                         circ            => $circ,
1060                         copy            => $copy,
1061                         route_to => $route_to,
1062                 };
1063         }
1064
1065         return 1;
1066
1067 }
1068
1069
1070
1071
1072
1073 # ------------------------------------------------------------------------------
1074 # RENEWALS
1075 # ------------------------------------------------------------------------------
1076
1077
1078 __PACKAGE__->register_method(
1079         method  => "renew",
1080         api_name        => "open-ils.circ.renew",
1081         notes           => <<"  NOTES");
1082         open-ils.circ.renew(login_session, circ_object);
1083         Renews the provided circulation.  login_session is the requestor of the
1084         renewal and if the logged in user is not the same as circ->usr, then
1085         the logged in user must have RENEW_CIRC permissions.
1086         NOTES
1087
1088 sub renew {
1089         my($self, $client, $login_session, $circ) = @_;
1090
1091         my( $evt, $user );
1092
1093         throw OpenSRF::EX::InvalidArg 
1094                 ("open-ils.circ.renew no circ") unless defined($circ);
1095
1096         ($user, $evt) = $apputils->checkses($login_session);
1097         return $evt if $evt;
1098
1099         my $session = OpenSRF::AppSession->create("open-ils.storage");
1100         my $copy = _grab_copy_by_id($session, $circ->target_copy);
1101
1102         my $r = $session->request(
1103                 "open-ils.storage.direct.action.hold_copy_map.search.target_copy.atomic",
1104                 $copy->id )->gather(1);
1105
1106         my @holdids = map { $_->hold  } @$r;
1107
1108         if(@$r != 0) { 
1109
1110                 my $holds = $session->request(
1111                         "open-ils.storage.direct.action.hold_request.search_where", 
1112                                 { id => \@holdids, current_copy => undef } )->gather(1);
1113
1114                 if( $holds and $user->id ne $circ->usr ) {
1115                         if($apputils->check_user_perms($user->id, $user->home_ou, "RENEW_HOLD_OVERRIDE")) {
1116                                 return OpenILS::Perm->new("RENEW_HOLD_OVERRIDE");
1117                         }
1118                 }
1119
1120                 return OpenILS::EX->new("COPY_NEEDED_FOR_HOLD")->ex; 
1121         }
1122
1123
1124         if(!ref($circ)) {
1125                 $circ = $session->request(
1126                         "open-ils.storage.direct.action.circulation.retrieve", $circ )->gather(1);
1127         }
1128
1129         my $iid = $circ->id;
1130         warn "Attempting to renew circ " . $iid . "\n";
1131
1132         if($user->id ne $circ->usr) {
1133                 if($apputils->check_user_perms($user->id, $user->home_ou, "RENEW_CIRC")) {
1134                         return OpenILS::Perm->new("RENEW_CIRC");
1135                 }
1136         }
1137
1138         if($circ->renewal_remaining <= 0) {
1139                 return OpenILS::EX->new("MAX_RENEWALS_REACHED")->ex; }
1140
1141
1142
1143         # XXX XXX See if the copy this circ points to is needed to fulfill a hold!
1144         # XXX check overdue..?
1145
1146         use Data::Dumper;
1147         my $checkin = $self->method_lookup("open-ils.circ.checkin.barcode");
1148         my ($status) = $checkin->run($login_session, $copy->barcode, 1);
1149         warn 'checkin status: ' . Dumper($status) . '\n';
1150         return $status if ref($status) eq "Fieldmapper::perm_ex";
1151         return $status if ($status->{status} ne "0"); 
1152         warn "Renewal checkin completed for $iid\n";
1153
1154         my $permit_checkout = $self->method_lookup("open-ils.circ.permit_checkout");
1155         ($status) = $permit_checkout->run($login_session, $copy->barcode, $circ->usr, "renew");
1156         warn 'permit checkout status: ' . Dumper($status) . '\n';
1157         return $status if ref($status) eq "Fieldmapper::perm_ex";
1158         return $status if($status->{status} ne "0");
1159         warn "Renewal permit checkout completed for $iid\n";
1160
1161         my $checkout = $self->method_lookup("open-ils.circ.checkout.barcode");
1162         ($status) = $checkout->run($login_session, $copy->barcode, $circ->usr, 1, $circ->renewal_remaining);
1163         warn 'renew checkout status: ' . Dumper($status) . '\n';
1164         return $status if ref($status) eq "Fieldmapper::perm_ex";
1165         warn "Renewal checkout completed for $iid\n";
1166         return $status;
1167
1168 }
1169
1170
1171
1172 1;