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