1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
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.
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 # ---------------------------------------------------------------
16 package OpenILS::Application::Circ::Rules;
17 use base qw/OpenSRF::Application/;
18 use strict; use warnings;
20 use OpenSRF::Utils::SettingsClient;
21 use OpenILS::Utils::Fieldmapper;
24 use Template qw(:template);
27 use Time::HiRes qw(time);
28 use OpenILS::Utils::ModsParser;
31 use OpenSRF::EX qw(:try);
33 use OpenILS::Application::AppUtils;
34 my $apputils = "OpenILS::Application::AppUtils";
35 use Digest::MD5 qw(md5_hex);
37 # ----------------------------------------------------------------
40 my $permission_script;
42 my $recurring_fines_script;
44 my $permit_hold_script;
45 # ----------------------------------------------------------------
48 # data used for this circulation transaction
49 my $circ_objects = {};
51 # some static data from the database
55 my $shelving_locations;
60 # memcache for caching the circ objects
64 use constant NO_COPY => 100;
68 my $conf = OpenSRF::Utils::SettingsClient->new;
70 # ----------------------------------------------------------------
71 # set up the rules scripts
72 # ----------------------------------------------------------------
73 $circ_script = $conf->config_value(
74 "apps", "open-ils.circ","app_settings", "rules", "main");
76 $permission_script = $conf->config_value(
77 "apps", "open-ils.circ","app_settings", "rules", "permission");
79 $duration_script = $conf->config_value(
80 "apps", "open-ils.circ","app_settings", "rules", "duration");
82 $recurring_fines_script = $conf->config_value(
83 "apps", "open-ils.circ","app_settings", "rules", "recurring_fines");
85 $max_fines_script = $conf->config_value(
86 "apps", "open-ils.circ","app_settings", "rules", "max_fines");
88 $permit_hold_script = $conf->config_value(
89 "apps", "open-ils.circ","app_settings", "rules", "permit_hold");
92 $cache_handle = OpenSRF::Utils::Cache->new();
96 sub _grab_patron_standings {
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);
103 { map { (''.$_->id => $_->value) } @$patron_standings };
107 sub _grab_patron_profiles {
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);
114 { map { (''.$_->id => $_->name) } @$patron_profiles };
121 my $patron_id = shift;
122 my $patron_req = $session->request(
123 "open-ils.storage.direct.actor.user.retrieve",
125 return $patron_req->gather(1);
129 sub _grab_title_by_copy {
132 my $title_req = $session->request(
133 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
135 return $title_req->gather(1);
138 sub _grab_patron_summary {
140 my $patron_id = shift;
141 my $summary_req = $session->request(
142 "open-ils.storage.action.circulation.patron_summary",
144 return $summary_req->gather(1);
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",
153 return $copy_req->gather(1);
157 sub gather_hold_objects {
158 my($session, $hold, $copy) = @_;
160 _grab_patron_standings($session);
161 _grab_patron_profiles($session);
165 $copy = _grab_copy_by_barcode($session, $copy->barcode);
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);
175 $copy->status( $copy->status->name );
176 $patron->standing($patron_standings->{$patron->standing()});
177 $patron->profile( $patron_profiles->{$patron->profile});
179 $hold_objects->{patron} = $patron;
181 return $hold_objects;
186 __PACKAGE__->register_method(
187 method => "permit_hold",
188 api_name => "open-ils.circ.permit_hold",
190 Determines whether a given copy is eligible to be held
195 my( $self, $client, $hold, $copy ) = @_;
197 my $session = OpenSRF::AppSession->create("open-ils.storage");
199 # collect items necessary for circ calculation
200 my $hold_objects = gather_hold_objects( $session, $hold, $copy );
202 $stash = Template::Stash->new(
203 circ_objects => $hold_objects,
206 $stash->set("run_block", $permit_hold_script);
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);
212 $stash->set("patron_copies", $summary->[0] );
213 $stash->set("patron_fines", $summary->[1] );
215 # run the permissibility script
217 my $result = $stash->get("result");
219 # 0 means OK in the script
220 return 1 if($result->[0] == 0);
229 # ----------------------------------------------------------------
230 # Collect all of the objects necessary for calculating the
232 # ----------------------------------------------------------------
233 sub gather_circ_objects {
234 my( $session, $barcode_string, $patron_id ) = @_;
236 throw OpenSRF::EX::ERROR
237 ("gather_circ_objects needs data")
238 unless ($barcode_string and $patron_id);
240 warn "Gathering circ objects with barcode $barcode_string and patron id $patron_id\n";
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);
247 $stash = Template::Stash->new(
248 circ_objects => $circ_objects,
250 target_copy_status => 1,
255 # only necessary if the circ objects have not been built yet
257 _grab_patron_standings($session);
258 _grab_patron_profiles($session);
261 my $copy = _grab_copy_by_barcode($session, $barcode_string);
262 if(!$copy) { return NO_COPY; }
264 my $patron = _grab_user($session, $patron_id);
266 $copy->status( $copy->status->name );
267 $circ_objects->{copy} = $copy;
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;
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 );
278 $stash = Template::Stash->new(
279 circ_objects => $circ_objects,
281 target_copy_status => 1,
291 my $template = Template->new(
299 my $status = $template->process($circ_script);
302 throw OpenSRF::EX::ERROR
303 ("Error processing circ script " . $template->error());
306 warn "Script result: $result\n";
312 __PACKAGE__->register_method(
313 method => "permit_circ",
314 api_name => "open-ils.circ.permit_checkout",
318 my( $self, $client, $user_session, $barcode, $user_id, $outstanding_count ) = @_;
320 $outstanding_count ||= 0;
322 my $session = OpenSRF::AppSession->create("open-ils.storage");
324 # collect items necessary for circ calculation
325 my $status = gather_circ_objects( $session, $barcode, $user_id );
327 if( $status == NO_COPY ) {
328 return { record => undef,
330 text => "No copy available with barcode $barcode"
334 $stash->set("run_block", $permission_script);
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);
343 $stash->set("patron_copies", $summary->[0] + $outstanding_count );
344 $stash->set("patron_fines", $summary->[1] );
346 # run the permissibility script
348 my $obj = $stash->get("circ_objects");
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();
355 my $arr = $stash->get("result");
356 return { record => $mods, status => $arr->[0], text => $arr->[1] };
362 __PACKAGE__->register_method(
363 method => "circulate",
364 api_name => "open-ils.circ.checkout.barcode",
368 my( $self, $client, $user_session, $barcode, $patron ) = @_;
371 my $session = $apputils->start_db_session();
373 gather_circ_objects( $session, $barcode, $patron );
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);
382 # put copy statuses into the stash
383 $stash->set("copy_statuses", $copy_statuses );
385 my $copy = $circ_objects->{copy};
386 my ($circ, $duration, $recurring, $max) = run_circ_scripts($session);
388 # commit new circ object to db
389 my $commit = $session->request(
390 "open-ils.storage.direct.action.circulation.create",
392 my $id = $commit->gather(1);
395 throw OpenSRF::EX::ERROR
396 ("Error creating new circulation object");
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 );
405 my $copy_update = $session->request(
406 "open-ils.storage.direct.asset.copy.update",
408 $copy_update->gather(1);
410 $apputils->commit_db_session($session);
412 # remove our circ object from the cache
413 $cache_handle->delete_cache("circ_object_" . md5_hex($barcode, $patron));
415 # re-retrieve the the committed circ object
416 $circ = $apputils->simple_scalar_request(
418 "open-ils.storage.direct.action.circulation.retrieve",
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);
429 # OpenSRF::Utils->interval_to_seconds(
430 # $circ->duration ) + int(time());
432 # this comes from an earlier setting now
433 # $circ->due_date($due_date);
441 # runs the duration, recurring_fines, and max_fines scripts.
442 # builds the new circ object based on the rules returned from
444 # returns (circ, duration_rule, recurring_fines_rule, max_fines_rule)
445 sub run_circ_scripts {
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);
454 my $duration_rule = $stash->get("result");
456 $stash->set("run_block", $recurring_fines_script);
457 $stash->set("result", [] );
459 my $rec_fines_rule = $stash->get("result");
461 $stash->set("run_block", $max_fines_script);
462 $stash->set("result", [] );
464 my $max_fines_rule = $stash->get("result");
466 my $obj = $stash->get("circ_objects");
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] );
475 my $rec_req = $session->request(
476 "open-ils.storage.direct.config.rules.recuring_fine.search.name.atomic",
477 $rec_fines_rule->[0] );
479 my $max_req = $session->request(
480 "open-ils.storage.direct.config.rules.max_fine.search.name.atomic",
481 $max_fines_rule->[0] );
483 my $duration = $dur_req->gather(1)->[0];
484 my $recurring = $rec_req->gather(1)->[0];
485 my $max = $max_req->gather(1)->[0];
487 my $copy = $circ_objects->{copy};
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);
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 );
503 return ($circ, $duration, $recurring, $max);
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 ) = @_;
514 my $circ = new Fieldmapper::action::circulation;
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 );
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 );
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 );
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 );
547 __PACKAGE__->register_method(
549 api_name => "open-ils.circ.checkin.barcode",
553 my( $self, $client, $user_session, $barcode ) = @_;
559 my $session = $apputils->start_db_session();
561 warn "retrieving copy for checkin\n";
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 };
571 my $copy_req = $session->request(
572 "open-ils.storage.direct.asset.copy.search.barcode.atomic",
574 $copy = $copy_req->gather(1)->[0];
576 $client->respond_complete(
577 OpenILS::EX->new("UNKNOWN_BARCODE")->ex);
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 } );
589 my $circ = $circ_req->gather(1)->[0];
592 $err = "No circulation exists for the given barcode";
596 warn "Checking in circ ". $circ->id . "\n";
598 $circ->stop_fines("CHECKIN");
599 $circ->xact_finish("now");
601 my $cp_up = $session->request(
602 "open-ils.storage.direct.asset.copy.update",
606 my $ci_up = $session->request(
607 "open-ils.storage.direct.action.circulation.update",
611 $apputils->commit_db_session($session);
613 warn "Checkin succeeded\n";
618 $err = "Error checking in: $e";
622 return { record => undef, status => -1, text => $err };
626 my $record = $apputils->simple_scalar_request(
628 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
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} };