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