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 my $permit_renew_script;
49 # ----------------------------------------------------------------
52 # data used for this circulation transaction
53 my $circ_objects = {};
55 # some static data from the database
59 my $shelving_locations;
64 # memcache for caching the circ objects
68 use constant NO_COPY => 100;
73 my $conf = OpenSRF::Utils::SettingsClient->new;
75 # ----------------------------------------------------------------
76 # set up the rules scripts
77 # ----------------------------------------------------------------
78 $circ_script = $conf->config_value(
79 "apps", "open-ils.circ","app_settings", "rules", "main");
81 $permission_script = $conf->config_value(
82 "apps", "open-ils.circ","app_settings", "rules", "permission");
84 $duration_script = $conf->config_value(
85 "apps", "open-ils.circ","app_settings", "rules", "duration");
87 $recurring_fines_script = $conf->config_value(
88 "apps", "open-ils.circ","app_settings", "rules", "recurring_fines");
90 $max_fines_script = $conf->config_value(
91 "apps", "open-ils.circ","app_settings", "rules", "max_fines");
93 $permit_hold_script = $conf->config_value(
94 "apps", "open-ils.circ","app_settings", "rules", "permit_hold");
96 $permit_renew_script = $conf->config_value(
97 "apps", "open-ils.circ","app_settings", "rules", "permit_renew");
99 $log->debug("Loaded rules scripts for circ:\n".
100 "main - $circ_script : permit circ - $permission_script\n".
101 "duration - $duration_script : recurring - $recurring_fines_script\n".
102 "max fines - $max_fines_script : permit hold - $permit_hold_script", DEBUG);
105 $cache_handle = OpenSRF::Utils::Cache->new();
109 sub _grab_patron_standings {
111 if(!$patron_standings) {
112 my $standing_req = $session->request(
113 "open-ils.storage.direct.config.standing.retrieve.all.atomic");
114 $patron_standings = $standing_req->gather(1);
116 { map { (''.$_->id => $_->value) } @$patron_standings };
120 sub _grab_patron_profiles {
122 if(!$patron_profiles) {
123 my $profile_req = $session->request(
124 "open-ils.storage.direct.actor.profile.retrieve.all.atomic");
125 $patron_profiles = $profile_req->gather(1);
127 { map { (''.$_->id => $_->name) } @$patron_profiles };
134 my $patron_id = shift;
135 my $patron_req = $session->request(
136 "open-ils.storage.direct.actor.user.retrieve",
138 return $patron_req->gather(1);
142 sub _grab_title_by_copy {
145 my $title_req = $session->request(
146 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
148 return $title_req->gather(1);
151 sub _grab_patron_summary {
153 my $patron_id = shift;
154 my $summary_req = $session->request(
155 "open-ils.storage.action.circulation.patron_summary",
157 return $summary_req->gather(1);
160 sub _grab_copy_by_barcode {
161 my($session, $barcode) = @_;
162 warn "Searching for copy with barcode $barcode\n";
163 my $copy_req = $session->request(
164 "open-ils.storage.fleshed.asset.copy.search.barcode",
166 return $copy_req->gather(1);
169 sub _grab_copy_by_id {
170 my($session, $id) = @_;
171 warn "Searching for copy with id $id\n";
172 my $copy_req = $session->request(
173 "open-ils.storage.direct.asset.copy.retrieve",
175 my $c = $copy_req->gather(1);
176 if($c) { return _grab_copy_by_barcode($session, $c->barcode); }
181 sub gather_hold_objects {
182 my($session, $hold, $copy, $args) = @_;
184 _grab_patron_standings($session);
185 _grab_patron_profiles($session);
189 $copy = _grab_copy_by_barcode($session, $copy->barcode);
191 my $hold_objects = {};
192 $hold_objects->{standings} = $patron_standings;
193 $hold_objects->{copy} = $copy;
194 $hold_objects->{hold} = $hold;
195 $hold_objects->{title} = $$args{title} || _grab_title_by_copy($session, $copy->id);
196 $hold_objects->{requestor} = _grab_user($session, $hold->requestor);
197 my $patron = _grab_user($session, $hold->usr);
199 $copy->status( $copy->status->name );
200 $patron->standing($patron_standings->{$patron->standing()});
201 $patron->profile( $patron_profiles->{$patron->profile});
203 $hold_objects->{patron} = $patron;
205 return $hold_objects;
210 __PACKAGE__->register_method(
211 method => "permit_hold",
212 api_name => "open-ils.circ.permit_hold",
213 notes => <<" NOTES");
214 Determines whether a given copy is eligible to be held
218 my( $self, $client, $hold, $copy, $args ) = @_;
220 my $session = OpenSRF::AppSession->create("open-ils.storage");
222 # collect items necessary for circ calculation
223 my $hold_objects = gather_hold_objects( $session, $hold, $copy, $args );
225 $stash = Template::Stash->new(
226 circ_objects => $hold_objects,
229 $stash->set("run_block", $permit_hold_script);
231 # grab the number of copies checked out by the patron as
232 # well as the total fines
233 my $summary = _grab_patron_summary($session, $hold_objects->{patron}->id);
235 $summary->[1] ||= 0.0;
237 $stash->set("patron_copies", $summary->[0] );
238 $stash->set("patron_fines", $summary->[1] );
240 # run the permissibility script
242 my $result = $stash->get("result");
244 # 0 means OK in the script
245 return 1 if($result->[0] == 0);
254 # ----------------------------------------------------------------
255 # Collect all of the objects necessary for calculating the
257 # ----------------------------------------------------------------
258 sub gather_circ_objects {
259 my( $session, $barcode_string, $patron_id ) = @_;
261 throw OpenSRF::EX::ERROR
262 ("gather_circ_objects needs data")
263 unless ($barcode_string and $patron_id);
265 warn "Gathering circ objects with barcode $barcode_string and patron id $patron_id\n";
267 # see if all of the circ objects are in cache
268 my $cache_key = "circ_object_" . md5_hex( $barcode_string, $patron_id );
269 $circ_objects = $cache_handle->get_cache($cache_key);
272 $stash = Template::Stash->new(
273 circ_objects => $circ_objects,
275 target_copy_status => 1,
280 # only necessary if the circ objects have not been built yet
282 _grab_patron_standings($session);
283 _grab_patron_profiles($session);
286 my $copy = _grab_copy_by_barcode($session, $barcode_string);
287 if(!$copy) { return NO_COPY; }
289 my $patron = _grab_user($session, $patron_id);
291 $copy->status( $copy->status->name );
292 $circ_objects->{copy} = $copy;
294 $patron->standing($patron_standings->{$patron->standing()});
295 $patron->profile( $patron_profiles->{$patron->profile});
296 $circ_objects->{patron} = $patron;
297 $circ_objects->{standings} = $patron_standings;
299 #$circ_objects->{title} = $title_req->gather(1);
300 $circ_objects->{title} = _grab_title_by_copy($session, $circ_objects->{copy}->id);
301 $cache_handle->put_cache( $cache_key, $circ_objects, 30 );
303 $stash = Template::Stash->new(
304 circ_objects => $circ_objects,
306 target_copy_status => 1,
316 my $template = Template->new(
324 my $status = $template->process($circ_script);
327 throw OpenSRF::EX::ERROR
328 ("Error processing circ script " . $template->error());
331 warn "Script result: $result\n";
337 __PACKAGE__->register_method(
338 method => "permit_circ",
339 api_name => "open-ils.circ.permit_checkout",
343 my( $self, $client, $user_session, $barcode, $user_id, $outstanding_count ) = @_;
345 my $copy_status_mangled;
348 if(defined($outstanding_count) && $outstanding_count eq "renew") {
350 $outstanding_count = 0;
351 } else { $outstanding_count ||= 0; }
353 my $session = OpenSRF::AppSession->create("open-ils.storage");
355 # collect items necessary for circ calculation
356 my $status = gather_circ_objects( $session, $barcode, $user_id );
358 if( $status == NO_COPY ) {
359 return { record => undef,
361 text => "No copy available with barcode $barcode"
364 my $copy = $stash->get("circ_objects")->{copy};
366 if( $copy->status eq "8" ) {
367 $copy_status_mangled = 8;
372 $stash->set("run_block", $permission_script);
374 # grab the number of copies checked out by the patron as
375 # well as the total fines
376 my $summary_req = $session->request(
377 "open-ils.storage.action.circulation.patron_summary",
378 $stash->get("circ_objects")->{patron}->id );
379 my $summary = $summary_req->gather(1);
382 $summary->[1] ||= 0.0;
384 $stash->set("patron_copies", $summary->[0] + $outstanding_count );
385 $stash->set("patron_fines", $summary->[1] );
386 $stash->set("renew", 1) if $renew;
388 # run the permissibility script
391 my $arr = $stash->get("result");
393 if( $arr->[0] eq "0" and $copy_status_mangled == 8) {
394 my $hold = $session->request(
395 "open-ils.storage.direct.action.hold_request.search.current_copy",
396 $copy->id )->gather(1);
398 if( $hold->usr eq $user_id ) {
399 return { status => 0, text => "OK" };
401 return { status => 6,
402 text => "Copy is needed by a different user to fulfill a hold" };
409 return { status => $arr->[0], text => $arr->[1] };
415 __PACKAGE__->register_method(
416 method => "circulate",
417 api_name => "open-ils.circ.checkout.barcode",
421 my( $self, $client, $user_session, $barcode, $patron, $isrenew, $numrenews ) = @_;
424 my $session = $apputils->start_db_session();
426 gather_circ_objects( $session, $barcode, $patron );
428 # grab the copy statuses if we don't already have them
429 if(!$copy_statuses) {
430 my $csreq = $session->request(
431 "open-ils.storage.direct.config.copy_status.retrieve.all.atomic" );
432 $copy_statuses = $csreq->gather(1);
435 # put copy statuses into the stash
436 $stash->set("copy_statuses", $copy_statuses );
438 my $copy = $circ_objects->{copy};
439 my ($circ, $duration, $recurring, $max) = run_circ_scripts($session);
442 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
443 gmtime(OpenSRF::Utils->interval_to_seconds($circ->duration) + int(time()));
444 $year += 1900; $mon += 1;
445 my $due_date = sprintf(
446 '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
447 $year, $mon, $mday, $hour, $min, $sec);
449 warn "Setting due date to $due_date\n";
450 $circ->due_date($due_date);
453 warn "Renewing circ.... ".$circ->id ." and setting num renews to " . $numrenews - 1 . "\n";
456 $circ->renewal_remaining($numrenews - 1);
460 # commit new circ object to db
461 my $commit = $session->request(
462 "open-ils.storage.direct.action.circulation.create",
464 my $id = $commit->gather(1);
467 throw OpenSRF::EX::ERROR
468 ("Error creating new circulation object");
471 # update the copy with the new circ
472 $copy->status( $stash->get("target_copy_status") );
473 $copy->location( $copy->location->id );
474 $copy->circ_lib( $copy->circ_lib->id ); #XXX XXX needs to point to the lib that actually checked out the item (user->home_ou)?
477 my $copy_update = $session->request(
478 "open-ils.storage.direct.asset.copy.update",
480 $copy_update->gather(1);
482 $apputils->commit_db_session($session);
484 # remove our circ object from the cache
485 $cache_handle->delete_cache("circ_object_" . md5_hex($barcode, $patron));
487 # re-retrieve the the committed circ object
488 $circ = $apputils->simple_scalar_request(
490 "open-ils.storage.direct.action.circulation.retrieve",
494 # push the rules and due date into the circ object
495 $circ->duration_rule($duration);
496 $circ->max_fine_rule($max);
497 $circ->recuring_fine_rule($recurring);
499 # turn the biblio record into a friendly object
500 my $obj = $stash->get("circ_objects");
501 my $u = OpenILS::Utils::ModsParser->new();
502 $u->start_mods_batch( $circ_objects->{title}->marc() );
503 my $mods = $u->finish_mods_batch();
506 return { circ => $circ, copy => $copy, record => $mods };
512 # runs the duration, recurring_fines, and max_fines scripts.
513 # builds the new circ object based on the rules returned from
515 # returns (circ, duration_rule, recurring_fines_rule, max_fines_rule)
516 sub run_circ_scripts {
519 # go through all of the scripts and process
520 # each script returns
521 # [ rule_name, level (appropriate to the script) ]
522 $stash->set("result", [] );
523 $stash->set("run_block", $duration_script);
525 my $duration_rule = $stash->get("result");
527 $stash->set("run_block", $recurring_fines_script);
528 $stash->set("result", [] );
530 my $rec_fines_rule = $stash->get("result");
532 $stash->set("run_block", $max_fines_script);
533 $stash->set("result", [] );
535 my $max_fines_rule = $stash->get("result");
537 my $obj = $stash->get("circ_objects");
539 # ----------------------------------------------------------
540 # find the rules objects based on the rule names returned from
541 # the various scripts.
542 my $dur_req = $session->request(
543 "open-ils.storage.direct.config.rules.circ_duration.search.name.atomic",
544 $duration_rule->[0] );
546 my $rec_req = $session->request(
547 "open-ils.storage.direct.config.rules.recuring_fine.search.name.atomic",
548 $rec_fines_rule->[0] );
550 my $max_req = $session->request(
551 "open-ils.storage.direct.config.rules.max_fine.search.name.atomic",
552 $max_fines_rule->[0] );
554 my $duration = $dur_req->gather(1)->[0];
555 my $recurring = $rec_req->gather(1)->[0];
556 my $max = $max_req->gather(1)->[0];
558 my $copy = $circ_objects->{copy};
561 warn "Building a new circulation object with\n".
562 "=> copy " . Dumper($copy) .
563 "=> duration_rule " . Dumper($duration_rule) .
564 "=> rec_files_rule " . Dumper($rec_fines_rule) .
565 "=> duration " . Dumper($duration) .
566 "=> recurring " . Dumper($recurring) .
567 "=> max " . Dumper($max);
570 # build the new circ object
571 my $circ = build_circ_object($session, $copy, $duration_rule->[1],
572 $rec_fines_rule->[1], $duration, $recurring, $max );
574 return ($circ, $duration, $recurring, $max);
578 # ------------------------------------------------------------------
579 # Builds a new circ object
580 # ------------------------------------------------------------------
581 sub build_circ_object {
582 my( $session, $copy, $dur_level, $rec_level,
583 $duration, $recurring, $max ) = @_;
585 my $circ = new Fieldmapper::action::circulation;
587 $circ->circ_lib( $copy->circ_lib->id() );
588 if($dur_level == 1) {
589 $circ->duration( $duration->shrt );
590 } elsif($dur_level == 2) {
591 $circ->duration( $duration->normal );
592 } elsif($dur_level == 3) {
593 $circ->duration( $duration->extended );
596 if($rec_level == 1) {
597 $circ->recuring_fine( $recurring->low );
598 } elsif($rec_level == 2) {
599 $circ->recuring_fine( $recurring->normal );
600 } elsif($rec_level == 3) {
601 $circ->recuring_fine( $recurring->high );
604 $circ->duration_rule( $duration->name );
605 $circ->recuring_fine_rule( $recurring->name );
606 $circ->max_fine_rule( $max->name );
607 $circ->max_fine( $max->amount );
609 $circ->fine_interval($recurring->recurance_interval);
610 $circ->renewal_remaining( $duration->max_renewals );
611 $circ->target_copy( $copy->id );
612 $circ->usr( $circ_objects->{patron}->id );
618 __PACKAGE__->register_method(
619 method => "transit_receive",
620 api_name => "open-ils.circ.transit.receive",
621 notes => <<" NOTES");
624 # status 3 means that this transit is destined for somewhere else
625 sub transit_receive {
626 my( $self, $client, $login_session, $copyid ) = @_;
628 my $user = $apputils->check_user_session($login_session);
630 my $session = $apputils->start_db_session();
631 my $copy = _grab_copy_by_id($session, $copyid);
634 if(!$copy->status eq "6") {
635 throw OpenSRF::EX::ERROR ("Copy is not in transit");
638 $transit = $session->request(
639 "open-ils.storage.direct.action.transit_copy.search_where",
640 { target_copy => $copy->id, dest_recv_time => undef } )->gather(1);
644 if($transit->dest ne $user->home_ou) {
645 return { status => 3, route_to => $transit->dest };
648 $transit->dest_recv_time("now");
649 my $s = $session->request(
650 "open-ils.storage.direct.action.transit_copy.update",
653 my $holdtransit = $session->request(
654 "open-ils.storage.direct.action.hold_transit_copy.retrieve",
659 my $hold = $session->request(
660 "open-ils.storage.direct.action.hold_request.retrieve",
661 $holdtransit->hold )->gather(1);
662 $copy->status(8); #hold shelf status
664 my $s = $session->request(
665 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
668 return { status => 0, route_to => $hold->pickup_lib };
671 } else { } #message...
677 __PACKAGE__->register_method(
679 api_name => "open-ils.circ.checkin.barcode",
680 notes => <<" NOTES");
681 Checks in based on barcode
682 Returns record, status, text, circ, copy, route_to
685 1 = 'copy required to fulfil a hold'
689 my( $self, $client, $user_session, $barcode, $isrenewal, $backdate ) = @_;
696 my $user = $apputils->check_user_session($user_session);
698 if($apputils->check_user_perms($user->id, $user->home_ou, "COPY_CHECKIN")) {
699 return OpenILS::Perm->new("COPY_CHECKIN");
702 my $session = $apputils->start_db_session();
708 warn "retrieving copy for checkin\n";
711 my $copy_req = $session->request(
712 "open-ils.storage.direct.asset.copy.search.barcode.atomic",
714 $copy = $copy_req->gather(1)->[0];
716 $client->respond_complete(OpenILS::EX->new("UNKNOWN_BARCODE")->ex);
719 if($copy->status eq "6") { #copy is in transit, deal with it
720 my $method = $self->method_lookup("open-ils.circ.transit.receive");
721 return $method->run( $user_session, $copy->id );
725 if(!$shelving_locations) {
726 my $sh_req = $session->request(
727 "open-ils.storage.direct.asset.copy_location.retrieve.all.atomic");
728 $shelving_locations = $sh_req->gather(1);
729 $shelving_locations =
730 { map { (''.$_->id => $_->name) } @$shelving_locations };
736 # find circ's where the transaction is still open for the
737 # given copy. should only be one.
738 warn "Retrieving circ for checkin\n";
739 my $circ_req = $session->request(
740 "open-ils.storage.direct.action.circulation.search.atomic",
741 { target_copy => $copy->id, xact_finish => undef } );
743 $circ = $circ_req->gather(1)->[0];
747 $err = "No circulation exists for the given barcode";
751 $transaction = $session->request(
752 "open-ils.storage.direct.money.billable_transaction_summary.retrieve", $circ->id)->gather(1);
754 warn "Checking in circ ". $circ->id . "\n";
756 $circ->stop_fines("CHECKIN");
757 $circ->stop_fines("RENEW") if($isrenewal);
758 $circ->xact_finish("now") if($transaction->balance_owed <= 0 );
760 my $cp_up = $session->request(
761 "open-ils.storage.direct.asset.copy.update", $copy );
764 my $ci_up = $session->request(
765 "open-ils.storage.direct.action.circulation.update",
770 warn "Checkin succeeded\n";
775 $err = "Error checking in: $e";
780 return { record => undef, status => -1, text => $err };
785 my $status_text = "OK";
787 # see if this copy can fulfill a hold
788 my $hold = OpenILS::Application::Circ::Holds::_find_local_hold_for_copy( $session, $copy, $user );
790 my $route_to = $shelving_locations->{$copy->location}
794 $status_text = "Copy needed to fulfill hold";
795 $route_to = $hold->pickup_lib;
798 $apputils->commit_db_session($session);
800 my $record = $apputils->simple_scalar_request(
802 "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy",
805 my $u = OpenILS::Utils::ModsParser->new();
806 $u->start_mods_batch( $record->marc() );
807 my $mods = $u->finish_mods_batch();
812 text => $status_text,
815 route_to => $routet_to,
827 # ------------------------------------------------------------------------------
829 # ------------------------------------------------------------------------------
832 __PACKAGE__->register_method(
834 api_name => "open-ils.circ.renew",
835 notes => <<" NOTES");
836 open-ils.circ.renew(login_session, circ_object);
837 Renews the provided circulation. login_session is the requestor of the
838 renewal and if the logged in user is not the same as circ->usr, then
839 the logged in user must have RENEW_CIRC permissions.
843 my($self, $client, $login_session, $circ) = @_;
845 throw OpenSRF::EX::InvalidArg
846 ("open-ils.circ.renew no circ") unless defined($circ);
848 my $user = $apputils->check_user_session($login_session);
850 my $session = OpenSRF::AppSession->create("open-ils.storage");
851 my $copy = _grab_copy_by_id($session, $circ->target_copy);
853 my $r = $session->request(
854 "open-ils.storage.direct.action.hold_copy_map.search.target_copy.atomic",
855 $copy->id )->gather(1);
857 my @holdids = map { $_->hold } @$r;
861 my $holds = $session->request(
862 "open-ils.storage.direct.action.hold_request.search_where",
863 { id => \@holdids, current_copy => undef } )->gather(1);
865 if( $holds and $user->id ne $circ->usr ) {
866 if($apputils->check_user_perms($user->id, $user->home_ou, "RENEW_HOLD_OVERRIDE")) {
867 return OpenILS::Perm->new("RENEW_HOLD_OVERRIDE");
871 return OpenILS::EX->new("COPY_NEEDED_FOR_HOLD")->ex;
876 $circ = $session->request(
877 "open-ils.storage.direct.action.circulation.retrieve", $circ )->gather(1);
881 warn "Attempting to renew circ " . $iid . "\n";
883 if($user->id ne $circ->usr) {
884 if($apputils->check_user_perms($user->id, $user->home_ou, "RENEW_CIRC")) {
885 return OpenILS::Perm->new("RENEW_CIRC");
889 if($circ->renewal_remaining <= 0) {
890 return OpenILS::EX->new("MAX_RENEWALS_REACHED")->ex; }
894 # XXX XXX See if the copy this circ points to is needed to fulfill a hold!
895 # XXX check overdue..?
897 my $checkin = $self->method_lookup("open-ils.circ.checkin.barcode");
898 my ($status) = $checkin->run($login_session, $copy->barcode, 1);
899 return $status if ($status->{status} ne "0");
900 warn "Renewal checkin completed for $iid\n";
902 my $permit_checkout = $self->method_lookup("open-ils.circ.permit_checkout");
903 ($status) = $permit_checkout->run($login_session, $copy->barcode, $circ->usr, "renew");
904 return $status if($status->{status} ne "0");
905 warn "Renewal permit checkout completed for $iid\n";
907 my $checkout = $self->method_lookup("open-ils.circ.checkout.barcode");
908 ($status) = $checkout->run($login_session, $copy->barcode, $circ->usr, 1, $circ->renewal_remaining);
909 warn "Renewal checkout completed for $iid\n";