]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Rules.pm
fixing some bugs, adding permission calls, adding some batch interfaces
[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 qw(: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 # ----------------------------------------------------------------
49
50
51 # data used for this circulation transaction
52 my $circ_objects = {};
53
54 # some static data from the database
55 my $copy_statuses;
56 my $patron_standings;
57 my $patron_profiles;
58 my $shelving_locations;
59
60 # template stash
61 my $stash;
62
63 # memcache for caching the circ objects
64 my $cache_handle;
65
66
67 use constant NO_COPY => 100;
68
69 sub initialize {
70
71         my $self = shift;
72         my $conf = OpenSRF::Utils::SettingsClient->new;
73
74         # ----------------------------------------------------------------
75         # set up the rules scripts
76         # ----------------------------------------------------------------
77         $circ_script = $conf->config_value(                                     
78                 "apps", "open-ils.circ","app_settings", "rules", "main");
79
80         $permission_script = $conf->config_value(                       
81                 "apps", "open-ils.circ","app_settings", "rules", "permission");
82
83         $duration_script = $conf->config_value(                 
84                 "apps", "open-ils.circ","app_settings", "rules", "duration");
85
86         $recurring_fines_script = $conf->config_value(  
87                 "apps", "open-ils.circ","app_settings", "rules", "recurring_fines");
88
89         $max_fines_script = $conf->config_value(                        
90                 "apps", "open-ils.circ","app_settings", "rules", "max_fines");
91
92         $permit_hold_script = $conf->config_value(
93                 "apps", "open-ils.circ","app_settings", "rules", "permit_hold");
94
95         $log->debug("Loaded rules scripts for circ:\n".
96                 "main - $circ_script : permit circ - $permission_script\n".
97                 "duration - $duration_script : recurring - $recurring_fines_script\n".
98                 "max fines - $max_fines_script : permit hold - $permit_hold_script", DEBUG);
99
100
101         $cache_handle = OpenSRF::Utils::Cache->new();
102 }
103
104
105 sub _grab_patron_standings {
106         my $session = shift;
107         if(!$patron_standings) {
108                 my $standing_req = $session->request(
109                         "open-ils.storage.direct.config.standing.retrieve.all.atomic");
110                 $patron_standings = $standing_req->gather(1);
111                 $patron_standings =
112                         { map { (''.$_->id => $_->value) } @$patron_standings };
113         }
114 }
115
116 sub _grab_patron_profiles {
117         my $session = shift;
118         if(!$patron_profiles) {
119                 my $profile_req = $session->request(
120                         "open-ils.storage.direct.actor.profile.retrieve.all.atomic");
121                 $patron_profiles = $profile_req->gather(1);
122                 $patron_profiles =
123                         { map { (''.$_->id => $_->name) } @$patron_profiles };
124         }
125
126 }
127
128 sub _grab_user {
129         my $session = shift;
130         my $patron_id = shift;
131         my $patron_req  = $session->request(
132                 "open-ils.storage.direct.actor.user.retrieve", 
133                 $patron_id );
134         return $patron_req->gather(1);
135 }
136         
137
138 sub _grab_title_by_copy {
139         my $session = shift;
140         my $copyid = shift;
141         my $title_req   = $session->request(
142                 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
143                 $copyid );
144         return $title_req->gather(1);
145 }
146
147 sub _grab_patron_summary {
148         my $session = shift;
149         my $patron_id = shift;
150         my $summary_req = $session->request(
151                 "open-ils.storage.action.circulation.patron_summary",
152                 $patron_id );
153         return $summary_req->gather(1);
154 }
155
156 sub _grab_copy_by_barcode {
157         my($session, $barcode) = @_;
158         warn "Searching for copy with barcode $barcode\n";
159         my $copy_req    = $session->request(
160                 "open-ils.storage.fleshed.asset.copy.search.barcode", 
161                 $barcode );
162         return $copy_req->gather(1);
163 }
164
165
166 sub gather_hold_objects {
167         my($session, $hold, $copy, $args) = @_;
168
169         _grab_patron_standings($session);
170         _grab_patron_profiles($session);
171
172
173         # flesh me
174         $copy = _grab_copy_by_barcode($session, $copy->barcode);
175
176         my $hold_objects = {};
177         $hold_objects->{standings} = $patron_standings;
178         $hold_objects->{copy}           = $copy;
179         $hold_objects->{hold}           = $hold;
180         $hold_objects->{title}          = $$args{title} || _grab_title_by_copy($session, $copy->id);
181         $hold_objects->{requestor} = _grab_user($session, $hold->requestor);
182         my $patron                                              = _grab_user($session, $hold->usr);
183
184         $copy->status( $copy->status->name );
185         $patron->standing($patron_standings->{$patron->standing()});
186         $patron->profile( $patron_profiles->{$patron->profile});
187
188         $hold_objects->{patron}         = $patron;
189
190         return $hold_objects;
191 }
192
193
194
195 __PACKAGE__->register_method(
196         method  => "permit_hold",
197         api_name        => "open-ils.circ.permit_hold",
198         notes           => <<NOTES
199 Determines whether a given copy is eligible to be held
200 NOTES
201 );
202
203 sub permit_hold {
204         my( $self, $client, $hold, $copy, $args ) = @_;
205
206         my $session     = OpenSRF::AppSession->create("open-ils.storage");
207         
208         # collect items necessary for circ calculation
209         my $hold_objects = gather_hold_objects( $session, $hold, $copy, $args );
210
211         $stash = Template::Stash->new(
212                         circ_objects                    => $hold_objects,
213                         result                                  => []);
214
215         $stash->set("run_block", $permit_hold_script);
216
217         # grab the number of copies checked out by the patron as
218         # well as the total fines
219         my $summary = _grab_patron_summary($session, $hold_objects->{patron}->id);
220         $summary->[0] ||= 0;
221         $summary->[0] ||= 0.0;
222
223         $stash->set("patron_copies", $summary->[0] );
224         $stash->set("patron_fines", $summary->[1] );
225
226         # run the permissibility script
227         run_script();
228         my $result = $stash->get("result");
229
230         # 0 means OK in the script
231         return 1 if($result->[0] == 0);
232         return 0;
233
234 }
235
236
237
238
239
240 # ----------------------------------------------------------------
241 # Collect all of the objects necessary for calculating the
242 # circ matrix.
243 # ----------------------------------------------------------------
244 sub gather_circ_objects {
245         my( $session, $barcode_string, $patron_id ) = @_;
246
247         throw OpenSRF::EX::ERROR 
248                 ("gather_circ_objects needs data")
249                         unless ($barcode_string and $patron_id);
250
251         warn "Gathering circ objects with barcode $barcode_string and patron id $patron_id\n";
252
253         # see if all of the circ objects are in cache
254         my $cache_key =  "circ_object_" . md5_hex( $barcode_string, $patron_id );
255         $circ_objects = $cache_handle->get_cache($cache_key);
256
257         if($circ_objects) { 
258                 $stash = Template::Stash->new(
259                         circ_objects                    => $circ_objects,
260                         result                                  => [],
261                         target_copy_status      => 1,
262                         );
263                 return;
264         }
265
266         # only necessary if the circ objects have not been built yet
267
268         _grab_patron_standings($session);
269         _grab_patron_profiles($session);
270
271
272         my $copy = _grab_copy_by_barcode($session, $barcode_string);
273         if(!$copy) { return NO_COPY; }
274
275         my $patron = _grab_user($session, $patron_id);
276
277         $copy->status( $copy->status->name );
278         $circ_objects->{copy} = $copy;
279
280         $patron->standing($patron_standings->{$patron->standing()});
281         $patron->profile( $patron_profiles->{$patron->profile});
282         $circ_objects->{patron} = $patron;
283         $circ_objects->{standings} = $patron_standings;
284
285         #$circ_objects->{title} = $title_req->gather(1);
286         $circ_objects->{title} = _grab_title_by_copy($session, $circ_objects->{copy}->id);
287         $cache_handle->put_cache( $cache_key, $circ_objects, 30 );
288
289         $stash = Template::Stash->new(
290                         circ_objects                    => $circ_objects,
291                         result                                  => [],
292                         target_copy_status      => 1,
293                         );
294 }
295
296
297
298 sub run_script {
299
300         my $result;
301
302         my $template = Template->new(
303                 { 
304                         STASH                   => $stash,
305                         ABSOLUTE                => 1, 
306                         OUTPUT          => \$result,
307                 }
308         );
309
310         my $status = $template->process($circ_script);
311
312         if(!$status) { 
313                 throw OpenSRF::EX::ERROR 
314                         ("Error processing circ script " .  $template->error()); 
315         }
316
317         warn "Script result: $result\n";
318 }
319
320
321
322
323 __PACKAGE__->register_method(
324         method  => "permit_circ",
325         api_name        => "open-ils.circ.permit_checkout",
326 );
327
328 sub permit_circ {
329         my( $self, $client, $user_session, $barcode, $user_id, $outstanding_count ) = @_;
330
331         $outstanding_count ||= 0;
332
333         my $session     = OpenSRF::AppSession->create("open-ils.storage");
334         
335         # collect items necessary for circ calculation
336         my $status = gather_circ_objects( $session, $barcode, $user_id );
337
338         if( $status == NO_COPY ) {
339                 return { record => undef, 
340                         status => NO_COPY, 
341                         text => "No copy available with barcode $barcode"
342                 };
343         }
344
345         $stash->set("run_block", $permission_script);
346
347         # grab the number of copies checked out by the patron as
348         # well as the total fines
349         my $summary_req = $session->request(
350                 "open-ils.storage.action.circulation.patron_summary",
351                 $stash->get("circ_objects")->{patron}->id );
352         my $summary = $summary_req->gather(1);
353
354         $stash->set("patron_copies", $summary->[0]  + $outstanding_count );
355         $stash->set("patron_fines", $summary->[1] );
356
357         # run the permissibility script
358         run_script();
359         my $obj = $stash->get("circ_objects");
360
361         # turn the biblio record into a friendly object
362         my $u = OpenILS::Utils::ModsParser->new();
363         $u->start_mods_batch( $obj->{title}->marc() );
364         my $mods = $u->finish_mods_batch();
365
366         my $arr = $stash->get("result");
367         return { record => $mods, status => $arr->[0], text => $arr->[1] };
368
369 }
370
371
372
373 __PACKAGE__->register_method(
374         method  => "circulate",
375         api_name        => "open-ils.circ.checkout.barcode",
376 );
377
378 sub circulate {
379         my( $self, $client, $user_session, $barcode, $patron ) = @_;
380
381
382         my $session = $apputils->start_db_session();
383
384         gather_circ_objects( $session, $barcode, $patron );
385
386         # grab the copy statuses if we don't already have them
387         if(!$copy_statuses) {
388                 my $csreq = $session->request(
389                         "open-ils.storage.direct.config.copy_status.retrieve.all.atomic" );
390                 $copy_statuses = $csreq->gather(1);
391         }
392
393         # put copy statuses into the stash
394         $stash->set("copy_statuses", $copy_statuses );
395
396         my $copy = $circ_objects->{copy};
397         my ($circ, $duration, $recurring, $max) =  run_circ_scripts($session);
398
399         # commit new circ object to db
400         my $commit = $session->request(
401                 "open-ils.storage.direct.action.circulation.create",
402                 $circ );
403         my $id = $commit->gather(1);
404
405         if(!$id) {
406                 throw OpenSRF::EX::ERROR 
407                         ("Error creating new circulation object");
408         }
409
410         # update the copy with the new circ
411         $copy->status( $stash->get("target_copy_status") );
412         $copy->location( $copy->location->id );
413         $copy->circ_lib( $copy->circ_lib->id );
414
415         # commit copy to db
416         my $copy_update = $session->request(
417                 "open-ils.storage.direct.asset.copy.update",
418                 $copy );
419         $copy_update->gather(1);
420
421         $apputils->commit_db_session($session);
422
423         # remove our circ object from the cache
424         $cache_handle->delete_cache("circ_object_" . md5_hex($barcode, $patron));
425
426         # re-retrieve the the committed circ object  
427         $circ = $apputils->simple_scalar_request(
428                 "open-ils.storage",
429                 "open-ils.storage.direct.action.circulation.retrieve",
430                 $id );
431
432
433         # push the rules and due date into the circ object
434         $circ->duration_rule($duration);
435         $circ->max_fine_rule($max);
436         $circ->recuring_fine_rule($recurring);
437
438
439 #       my $due_date = 
440 #               OpenSRF::Utils->interval_to_seconds( 
441 #               $circ->duration ) + int(time());
442
443 #       this comes from an earlier setting now
444 #       $circ->due_date($due_date);
445
446         return $circ;
447
448 }
449
450
451
452 # runs the duration, recurring_fines, and max_fines scripts.
453 # builds the new circ object based on the rules returned from 
454 # these scripts. 
455 # returns (circ, duration_rule, recurring_fines_rule, max_fines_rule)
456 sub run_circ_scripts {
457         my $session = shift;
458
459         # go through all of the scripts and process
460         # each script returns 
461         # [ rule_name, level (appropriate to the script) ]
462         $stash->set("result", [] );
463         $stash->set("run_block", $duration_script);
464         run_script();
465         my $duration_rule = $stash->get("result");
466
467         $stash->set("run_block", $recurring_fines_script);
468         $stash->set("result", [] );
469         run_script();
470         my $rec_fines_rule = $stash->get("result");
471
472         $stash->set("run_block", $max_fines_script);
473         $stash->set("result", [] );
474         run_script();
475         my $max_fines_rule = $stash->get("result");
476
477         my $obj = $stash->get("circ_objects");
478
479         # ----------------------------------------------------------
480         # find the rules objects based on the rule names returned from
481         # the various scripts.
482         my $dur_req = $session->request(
483                 "open-ils.storage.direct.config.rules.circ_duration.search.name.atomic",
484                 $duration_rule->[0] );
485
486         my $rec_req = $session->request(
487                 "open-ils.storage.direct.config.rules.recuring_fine.search.name.atomic",
488                 $rec_fines_rule->[0] );
489
490         my $max_req = $session->request(
491                 "open-ils.storage.direct.config.rules.max_fine.search.name.atomic",
492                 $max_fines_rule->[0] );
493
494         my $duration    = $dur_req->gather(1)->[0];
495         my $recurring   = $rec_req->gather(1)->[0];
496         my $max                 = $max_req->gather(1)->[0];
497
498         my $copy = $circ_objects->{copy};
499
500         use Data::Dumper;
501         warn "Building a new circulation object with\n".
502                 "=> copy "                              . Dumper($copy) .
503                 "=> duration_rule "     . Dumper($duration_rule) .
504                 "=> rec_files_rule " . Dumper($rec_fines_rule) .
505                 "=> duration "                  . Dumper($duration) .
506                 "=> recurring "         . Dumper($recurring) .
507                 "=> max "                               . Dumper($max);
508
509
510         # build the new circ object
511         my $circ =  build_circ_object($session, $copy, $duration_rule->[1], 
512                         $rec_fines_rule->[1], $duration, $recurring, $max );
513
514         return ($circ, $duration, $recurring, $max);
515
516 }
517
518 # ------------------------------------------------------------------
519 # Builds a new circ object
520 # ------------------------------------------------------------------
521 sub build_circ_object {
522         my( $session, $copy, $dur_level, $rec_level, 
523                         $duration, $recurring, $max ) = @_;
524
525         my $circ = new Fieldmapper::action::circulation;
526
527         $circ->circ_lib( $copy->circ_lib->id() );
528         if($dur_level == 1) {
529                 $circ->duration( $duration->shrt );
530         } elsif($dur_level == 2) {
531                 $circ->duration( $duration->normal );
532         } elsif($dur_level == 3) {
533                 $circ->duration( $duration->extended );
534         }
535
536         if($rec_level == 1) {
537                 $circ->recuring_fine( $recurring->low );
538         } elsif($rec_level == 2) {
539                 $circ->recuring_fine( $recurring->normal );
540         } elsif($rec_level == 3) {
541                 $circ->recuring_fine( $recurring->high );
542         }
543
544         $circ->duration_rule( $duration->name );
545         $circ->recuring_fine_rule( $recurring->name );
546         $circ->max_fine_rule( $max->name );
547         $circ->max_fine( $max->amount );
548
549         $circ->fine_interval($recurring->recurance_interval);
550         $circ->renewal_remaining( $duration->max_renewals );
551         $circ->target_copy( $copy->id );
552         $circ->usr( $circ_objects->{patron}->id );
553
554         return $circ;
555
556 }
557
558 __PACKAGE__->register_method(
559         method  => "checkin",
560         api_name        => "open-ils.circ.checkin.barcode",
561 );
562
563 sub checkin {
564         my( $self, $client, $user_session, $barcode ) = @_;
565
566         my $err;
567         my $copy;
568
569         try {
570                 my $session = $apputils->start_db_session();
571         
572                 warn "retrieving copy for checkin\n";
573
574                 if(!$shelving_locations) {
575                         my $sh_req = $session->request(
576                                 "open-ils.storage.direct.asset.copy_location.retrieve.all.atomic");
577                         $shelving_locations = $sh_req->gather(1);
578                         $shelving_locations = 
579                                 { map { (''.$_->id => $_->name) } @$shelving_locations };
580                 }
581         
582                 my $copy_req = $session->request(
583                         "open-ils.storage.direct.asset.copy.search.barcode.atomic", 
584                         $barcode );
585                 $copy = $copy_req->gather(1)->[0];
586                 if(!$copy) {
587                         $client->respond_complete(
588                                         OpenILS::EX->new("UNKNOWN_BARCODE")->ex);
589                 }
590
591                 $copy->status(0);
592         
593                 # find circ's where the transaction is still open for the
594                 # given copy.  should only be one.
595                 warn "Retrieving circ for checking\n";
596                 my $circ_req = $session->request(
597                         "open-ils.storage.direct.action.circulation.search.atomic.atomic",
598                         { target_copy => $copy->id, xact_finish => undef } );
599         
600                 my $circ = $circ_req->gather(1)->[0];
601         
602                 if(!$circ) {
603                         $err = "No circulation exists for the given barcode";
604
605                 } else {
606         
607                         warn "Checking in circ ". $circ->id . "\n";
608                 
609                         $circ->stop_fines("CHECKIN");
610                         $circ->xact_finish("now");
611                 
612                         my $cp_up = $session->request(
613                                 "open-ils.storage.direct.asset.copy.update",
614                                 $copy );
615                         $cp_up->gather(1);
616                 
617                         my $ci_up = $session->request(
618                                 "open-ils.storage.direct.action.circulation.update",
619                                 $circ );
620                         $ci_up->gather(1);
621                 
622                         $apputils->commit_db_session($session);
623                 
624                         warn "Checkin succeeded\n";
625                 }
626         
627         } catch Error with {
628                 my $e = shift;
629                 $err = "Error checking in: $e";
630         };
631         
632         if($err) {
633                 return { record => undef, status => -1, text => $err };
634
635         } else {
636
637                 my $record = $apputils->simple_scalar_request(
638                         "open-ils.storage",
639                         "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
640                         $copy->id() );
641
642                 my $u = OpenILS::Utils::ModsParser->new();
643                 $u->start_mods_batch( $record->marc() );
644                 my $mods = $u->finish_mods_batch();
645                 return { record => $mods, status => 0, text => "OK", 
646                         route_to => $shelving_locations->{$copy->location} };
647         }
648
649         return 1;
650
651 }
652
653
654
655
656
657 1;