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;
23 use OpenSRF::Utils::Logger qw(:level);
25 use Template qw(:template);
28 use Time::HiRes qw(time);
29 use OpenILS::Utils::ModsParser;
32 use OpenSRF::EX qw(:try);
34 use OpenILS::Application::AppUtils;
35 my $apputils = "OpenILS::Application::AppUtils";
36 use Digest::MD5 qw(md5_hex);
38 my $log = "OpenSRF::Utils::Logger";
40 # ----------------------------------------------------------------
43 my $permission_script;
45 my $recurring_fines_script;
47 my $permit_hold_script;
48 # ----------------------------------------------------------------
51 # data used for this circulation transaction
52 my $circ_objects = {};
54 # some static data from the database
58 my $shelving_locations;
63 # memcache for caching the circ objects
67 use constant NO_COPY => 100;
71 my $conf = OpenSRF::Utils::SettingsClient->new;
73 # ----------------------------------------------------------------
74 # set up the rules scripts
75 # ----------------------------------------------------------------
76 $circ_script = $conf->config_value(
77 "apps", "open-ils.circ","app_settings", "rules", "main");
79 $permission_script = $conf->config_value(
80 "apps", "open-ils.circ","app_settings", "rules", "permission");
82 $duration_script = $conf->config_value(
83 "apps", "open-ils.circ","app_settings", "rules", "duration");
85 $recurring_fines_script = $conf->config_value(
86 "apps", "open-ils.circ","app_settings", "rules", "recurring_fines");
88 $max_fines_script = $conf->config_value(
89 "apps", "open-ils.circ","app_settings", "rules", "max_fines");
91 $permit_hold_script = $conf->config_value(
92 "apps", "open-ils.circ","app_settings", "rules", "permit_hold");
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);
100 $cache_handle = OpenSRF::Utils::Cache->new();
104 sub _grab_patron_standings {
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);
111 { map { (''.$_->id => $_->value) } @$patron_standings };
115 sub _grab_patron_profiles {
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);
122 { map { (''.$_->id => $_->name) } @$patron_profiles };
129 my $patron_id = shift;
130 my $patron_req = $session->request(
131 "open-ils.storage.direct.actor.user.retrieve",
133 return $patron_req->gather(1);
137 sub _grab_title_by_copy {
140 my $title_req = $session->request(
141 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
143 return $title_req->gather(1);
146 sub _grab_patron_summary {
148 my $patron_id = shift;
149 my $summary_req = $session->request(
150 "open-ils.storage.action.circulation.patron_summary",
152 return $summary_req->gather(1);
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",
161 return $copy_req->gather(1);
165 sub gather_hold_objects {
166 my($session, $hold, $copy, $args) = @_;
168 _grab_patron_standings($session);
169 _grab_patron_profiles($session);
173 $copy = _grab_copy_by_barcode($session, $copy->barcode);
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);
183 $copy->status( $copy->status->name );
184 $patron->standing($patron_standings->{$patron->standing()});
185 $patron->profile( $patron_profiles->{$patron->profile});
187 $hold_objects->{patron} = $patron;
189 return $hold_objects;
194 __PACKAGE__->register_method(
195 method => "permit_hold",
196 api_name => "open-ils.circ.permit_hold",
198 Determines whether a given copy is eligible to be held
203 my( $self, $client, $hold, $copy, $args ) = @_;
205 my $session = OpenSRF::AppSession->create("open-ils.storage");
207 # collect items necessary for circ calculation
208 my $hold_objects = gather_hold_objects( $session, $hold, $copy, $args );
210 $stash = Template::Stash->new(
211 circ_objects => $hold_objects,
214 $stash->set("run_block", $permit_hold_script);
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);
220 $summary->[0] ||= 0.0;
222 $stash->set("patron_copies", $summary->[0] );
223 $stash->set("patron_fines", $summary->[1] );
225 # run the permissibility script
227 my $result = $stash->get("result");
229 # 0 means OK in the script
230 return 1 if($result->[0] == 0);
239 # ----------------------------------------------------------------
240 # Collect all of the objects necessary for calculating the
242 # ----------------------------------------------------------------
243 sub gather_circ_objects {
244 my( $session, $barcode_string, $patron_id ) = @_;
246 throw OpenSRF::EX::ERROR
247 ("gather_circ_objects needs data")
248 unless ($barcode_string and $patron_id);
250 warn "Gathering circ objects with barcode $barcode_string and patron id $patron_id\n";
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);
257 $stash = Template::Stash->new(
258 circ_objects => $circ_objects,
260 target_copy_status => 1,
265 # only necessary if the circ objects have not been built yet
267 _grab_patron_standings($session);
268 _grab_patron_profiles($session);
271 my $copy = _grab_copy_by_barcode($session, $barcode_string);
272 if(!$copy) { return NO_COPY; }
274 my $patron = _grab_user($session, $patron_id);
276 $copy->status( $copy->status->name );
277 $circ_objects->{copy} = $copy;
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;
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 );
288 $stash = Template::Stash->new(
289 circ_objects => $circ_objects,
291 target_copy_status => 1,
301 my $template = Template->new(
309 my $status = $template->process($circ_script);
312 throw OpenSRF::EX::ERROR
313 ("Error processing circ script " . $template->error());
316 warn "Script result: $result\n";
322 __PACKAGE__->register_method(
323 method => "permit_circ",
324 api_name => "open-ils.circ.permit_checkout",
328 my( $self, $client, $user_session, $barcode, $user_id, $outstanding_count ) = @_;
330 $outstanding_count ||= 0;
332 my $session = OpenSRF::AppSession->create("open-ils.storage");
334 # collect items necessary for circ calculation
335 my $status = gather_circ_objects( $session, $barcode, $user_id );
337 if( $status == NO_COPY ) {
338 return { record => undef,
340 text => "No copy available with barcode $barcode"
344 $stash->set("run_block", $permission_script);
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);
353 $stash->set("patron_copies", $summary->[0] + $outstanding_count );
354 $stash->set("patron_fines", $summary->[1] );
356 # run the permissibility script
358 my $obj = $stash->get("circ_objects");
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();
365 my $arr = $stash->get("result");
366 return { record => $mods, status => $arr->[0], text => $arr->[1] };
372 __PACKAGE__->register_method(
373 method => "circulate",
374 api_name => "open-ils.circ.checkout.barcode",
378 my( $self, $client, $user_session, $barcode, $patron ) = @_;
381 my $session = $apputils->start_db_session();
383 gather_circ_objects( $session, $barcode, $patron );
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);
392 # put copy statuses into the stash
393 $stash->set("copy_statuses", $copy_statuses );
395 my $copy = $circ_objects->{copy};
396 my ($circ, $duration, $recurring, $max) = run_circ_scripts($session);
398 # commit new circ object to db
399 my $commit = $session->request(
400 "open-ils.storage.direct.action.circulation.create",
402 my $id = $commit->gather(1);
405 throw OpenSRF::EX::ERROR
406 ("Error creating new circulation object");
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 );
415 my $copy_update = $session->request(
416 "open-ils.storage.direct.asset.copy.update",
418 $copy_update->gather(1);
420 $apputils->commit_db_session($session);
422 # remove our circ object from the cache
423 $cache_handle->delete_cache("circ_object_" . md5_hex($barcode, $patron));
425 # re-retrieve the the committed circ object
426 $circ = $apputils->simple_scalar_request(
428 "open-ils.storage.direct.action.circulation.retrieve",
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);
439 # OpenSRF::Utils->interval_to_seconds(
440 # $circ->duration ) + int(time());
442 # this comes from an earlier setting now
443 # $circ->due_date($due_date);
451 # runs the duration, recurring_fines, and max_fines scripts.
452 # builds the new circ object based on the rules returned from
454 # returns (circ, duration_rule, recurring_fines_rule, max_fines_rule)
455 sub run_circ_scripts {
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);
464 my $duration_rule = $stash->get("result");
466 $stash->set("run_block", $recurring_fines_script);
467 $stash->set("result", [] );
469 my $rec_fines_rule = $stash->get("result");
471 $stash->set("run_block", $max_fines_script);
472 $stash->set("result", [] );
474 my $max_fines_rule = $stash->get("result");
476 my $obj = $stash->get("circ_objects");
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] );
485 my $rec_req = $session->request(
486 "open-ils.storage.direct.config.rules.recuring_fine.search.name.atomic",
487 $rec_fines_rule->[0] );
489 my $max_req = $session->request(
490 "open-ils.storage.direct.config.rules.max_fine.search.name.atomic",
491 $max_fines_rule->[0] );
493 my $duration = $dur_req->gather(1)->[0];
494 my $recurring = $rec_req->gather(1)->[0];
495 my $max = $max_req->gather(1)->[0];
497 my $copy = $circ_objects->{copy};
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);
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 );
513 return ($circ, $duration, $recurring, $max);
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 ) = @_;
524 my $circ = new Fieldmapper::action::circulation;
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 );
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 );
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 );
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 );
557 __PACKAGE__->register_method(
559 api_name => "open-ils.circ.checkin.barcode",
563 my( $self, $client, $user_session, $barcode ) = @_;
569 my $session = $apputils->start_db_session();
571 warn "retrieving copy for checkin\n";
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 };
581 my $copy_req = $session->request(
582 "open-ils.storage.direct.asset.copy.search.barcode.atomic",
584 $copy = $copy_req->gather(1)->[0];
586 $client->respond_complete(
587 OpenILS::EX->new("UNKNOWN_BARCODE")->ex);
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 } );
599 my $circ = $circ_req->gather(1)->[0];
602 $err = "No circulation exists for the given barcode";
606 warn "Checking in circ ". $circ->id . "\n";
608 $circ->stop_fines("CHECKIN");
609 $circ->xact_finish("now");
611 my $cp_up = $session->request(
612 "open-ils.storage.direct.asset.copy.update",
616 my $ci_up = $session->request(
617 "open-ils.storage.direct.action.circulation.update",
621 $apputils->commit_db_session($session);
623 warn "Checkin succeeded\n";
628 $err = "Error checking in: $e";
632 return { record => undef, status => -1, text => $err };
636 my $record = $apputils->simple_scalar_request(
638 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
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} };