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 # ---------------------------------------------------------------
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenSRF::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
22 use OpenSRF::EX qw(:try);
25 use OpenSRF::Utils::Logger qw(:logger);
26 use OpenILS::Utils::Editor;
28 my $apputils = "OpenILS::Application::AppUtils";
33 __PACKAGE__->register_method(
34 method => "create_hold",
35 api_name => "open-ils.circ.holds.create",
37 Create a new hold for an item. From a permissions perspective,
38 the login session is used as the 'requestor' of the hold.
39 The hold recipient is determined by the 'usr' setting within
42 First we verify the requestion has holds request permissions.
43 Then we verify that the recipient is allowed to make the given hold.
44 If not, we see if the requestor has "override" capabilities. If not,
45 a permission exception is returned. If permissions allow, we cycle
46 through the set of holds objects and create.
48 If the recipient does not have permission to place multiple holds
49 on a single title and said operation is attempted, a permission
54 my( $self, $client, $login_session, @holds) = @_;
56 if(!@holds){return 0;}
57 my( $user, $evt ) = $apputils->checkses($login_session);
61 if(ref($holds[0]) eq 'ARRAY') {
63 } else { $holds = [ @holds ]; }
65 $logger->debug("Iterating over holds requests...");
67 for my $hold (@$holds) {
70 my $type = $hold->hold_type;
72 $logger->activity("User " . $user->id .
73 " creating new hold of type $type for user " . $hold->usr);
76 if($user->id ne $hold->usr) {
77 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
87 # am I allowed to place holds for this user?
88 if($hold->requestor ne $hold->usr) {
89 $perm = _check_request_holds_perm($user->id, $user->home_ou);
90 if($perm) { return $perm; }
93 # is this user allowed to have holds of this type?
94 $perm = _check_holds_perm($type, $hold->usr, $recipient->home_ou);
96 #if there is a requestor, see if the requestor has override privelages
97 if($hold->requestor ne $hold->usr) {
98 $perm = _check_request_holds_override($user->id, $user->home_ou);
99 if($perm) {return $perm;}
106 #enforce the fact that the login is the one requesting the hold
107 $hold->requestor($user->id);
108 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
110 my $resp = $apputils->simplereq(
112 'open-ils.storage.direct.action.hold_request.create', $hold );
115 return OpenSRF::EX::ERROR ("Error creating hold");
122 # makes sure that a user has permission to place the type of requested hold
123 # returns the Perm exception if not allowed, returns undef if all is well
124 sub _check_holds_perm {
125 my($type, $user_id, $org_id) = @_;
129 if($evt = $apputils->check_perms(
130 $user_id, $org_id, "MR_HOLDS")) {
134 } elsif ($type eq "T") {
135 if($evt = $apputils->check_perms(
136 $user_id, $org_id, "TITLE_HOLDS")) {
140 } elsif($type eq "V") {
141 if($evt = $apputils->check_perms(
142 $user_id, $org_id, "VOLUME_HOLDS")) {
146 } elsif($type eq "C") {
147 if($evt = $apputils->check_perms(
148 $user_id, $org_id, "COPY_HOLDS")) {
156 # tests if the given user is allowed to place holds on another's behalf
157 sub _check_request_holds_perm {
160 if(my $evt = $apputils->check_perms(
161 $user_id, $org_id, "REQUEST_HOLDS")) {
166 sub _check_request_holds_override {
169 if(my $evt = $apputils->check_perms(
170 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
175 __PACKAGE__->register_method(
176 method => "retrieve_holds_by_id",
177 api_name => "open-ils.circ.holds.retrieve_by_id",
179 Retrieve the hold, with hold transits attached, for the specified id
180 The login session is the requestor and if the requestor is
181 different from the user, then the requestor must have VIEW_HOLD permissions.
185 sub retrieve_holds_by_id {
186 my($self, $client, $login_session, $hold_id) = @_;
189 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
190 # $login_session, $user_id, 'VIEW_HOLD' );
191 #return $evt if $evt;
193 my $holds = $apputils->simplereq(
195 "open-ils.storage.direct.action.hold_request.search.atomic",
196 "id" => $hold_id , fulfillment_time => undef, { order_by => "request_time" });
198 for my $hold ( @$holds ) {
200 $apputils->simplereq(
202 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
203 { order_by => 'id desc', limit => 1 }
212 __PACKAGE__->register_method(
213 method => "retrieve_holds",
214 api_name => "open-ils.circ.holds.retrieve",
216 Retrieves all the holds, with hold transits attached, for the specified
217 user id. The login session is the requestor and if the requestor is
218 different from the user, then the requestor must have VIEW_HOLD permissions.
223 my($self, $client, $login_session, $user_id) = @_;
225 my( $user, $target, $evt ) = $apputils->checkses_requestor(
226 $login_session, $user_id, 'VIEW_HOLD' );
229 my $holds = $apputils->simplereq(
231 "open-ils.storage.direct.action.hold_request.search.atomic",
232 "usr" => $user_id , fulfillment_time => undef, { order_by => "request_time" });
234 for my $hold ( @$holds ) {
236 $apputils->simplereq(
238 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
239 { order_by => 'id desc', limit => 1 }
247 __PACKAGE__->register_method(
248 method => "retrieve_holds_by_pickup_lib",
249 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
251 Retrieves all the holds, with hold transits attached, for the specified
256 sub retrieve_holds_by_pickup_lib {
257 my($self, $client, $login_session, $ou_id) = @_;
259 #FIXME -- put an appropriate permission check here
260 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
261 # $login_session, $user_id, 'VIEW_HOLD' );
262 #return $evt if $evt;
264 my $holds = $apputils->simplereq(
266 "open-ils.storage.direct.action.hold_request.search.atomic",
267 "pickup_lib" => $ou_id , fulfillment_time => undef, { order_by => "request_time" });
269 for my $hold ( @$holds ) {
271 $apputils->simplereq(
273 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
274 { order_by => 'id desc', limit => 1 }
283 __PACKAGE__->register_method(
284 method => "cancel_hold",
285 api_name => "open-ils.circ.hold.cancel",
287 Cancels the specified hold. The login session
288 is the requestor and if the requestor is different from the usr field
289 on the hold, the requestor must have CANCEL_HOLDS permissions.
290 the hold may be either the hold object or the hold id
294 my($self, $client, $login_session, $holdid) = @_;
297 my($user, $evt) = $U->checkses($login_session);
300 ( $hold, $evt ) = $U->fetch_hold($holdid);
303 if($user->id ne $hold->usr) { #am I allowed to cancel this user's hold?
304 if($evt = $apputils->check_perms(
305 $user->id, $user->home_ou, 'CANCEL_HOLDS')) {
310 $logger->activity( "User " . $user->id .
311 " canceling hold $holdid for user " . $hold->usr );
313 return $apputils->simplereq(
315 "open-ils.storage.direct.action.hold_request.delete", $hold );
319 __PACKAGE__->register_method(
320 method => "update_hold",
321 api_name => "open-ils.circ.hold.update",
323 Updates the specified hold. The login session
324 is the requestor and if the requestor is different from the usr field
325 on the hold, the requestor must have UPDATE_HOLDS permissions.
329 my($self, $client, $login_session, $hold) = @_;
331 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
332 $login_session, $hold->usr, 'UPDATE_HOLD' );
335 $logger->activity('User ' . $requestor->id .
336 ' updating hold ' . $hold->id . ' for user ' . $target->id );
338 return $U->storagereq(
339 "open-ils.storage.direct.action.hold_request.update", $hold );
343 __PACKAGE__->register_method(
344 method => "retrieve_hold_status",
345 api_name => "open-ils.circ.hold.status.retrieve",
347 Calculates the current status of the hold.
348 the requestor must have VIEW_HOLD permissions if the hold is for a user
349 other than the requestor.
350 Returns -1 on error (for now)
351 Returns 1 for 'waiting for copy to become available'
352 Returns 2 for 'waiting for copy capture'
353 Returns 3 for 'in transit'
354 Returns 4 for 'arrived'
357 sub retrieve_hold_status {
358 my($self, $client, $login_session, $hold_id) = @_;
361 my( $requestor, $target, $hold, $copy, $transit, $evt );
363 ( $hold, $evt ) = $apputils->fetch_hold($hold_id);
366 ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
367 $login_session, $hold->usr, 'VIEW_HOLD' );
370 return 1 unless (defined($hold->current_copy));
372 ( $copy, $evt ) = $apputils->fetch_copy($hold->current_copy);
375 return 4 if ($hold->capture_time and $copy->circ_lib eq $hold->pickup_lib);
377 ( $transit, $evt ) = $apputils->fetch_hold_transit_by_hold( $hold->id );
378 return 4 if(ref($transit) and defined($transit->dest_recv_time) );
380 return 3 if defined($hold->capture_time);
389 __PACKAGE__->register_method(
390 method => "capture_copy",
391 api_name => "open-ils.circ.hold.capture_copy.barcode",
393 Captures a copy to fulfil a hold
394 Params is login session and copy barcode
395 Optional param is 'flesh'. If set, we also return the
396 relevant copy and title
397 login mus have COPY_CHECKIN permissions (since this is essentially
401 # XXX deprecate me XXX
404 my( $self, $client, $login_session, $params ) = @_;
405 my %params = %$params;
406 my $barcode = $params{barcode};
409 my( $user, $target, $copy, $hold, $evt );
411 ( $user, $evt ) = $apputils->checkses($login_session);
414 # am I allowed to checkin a copy?
415 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
418 $logger->info("Capturing copy with barcode $barcode");
420 my $session = $apputils->start_db_session();
422 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
425 $logger->debug("Capturing copy " . $copy->id);
427 ( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
430 warn "Found hold " . $hold->id . "\n";
431 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
433 $hold->current_copy($copy->id);
434 $hold->capture_time("now");
437 my $stat = $session->request(
438 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
439 if(!$stat) { throw OpenSRF::EX::ERROR
440 ("Error updating hold request " . $copy->id); }
442 $copy->status(8); #status on holds shelf
444 # if the staff member capturing this item is not at the pickup lib
445 if( $user->home_ou ne $hold->pickup_lib ) {
446 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
449 $copy->editor($user->id);
450 $copy->edit_date("now");
451 $stat = $session->request(
452 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
453 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
455 my $payload = { hold => $hold };
456 $payload->{copy} = $copy if $params{flesh_copy};
458 if($params{flesh_record}) {
460 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
462 $record = $apputils->record_to_mvr($record);
463 $payload->{record} = $record;
466 $apputils->commit_db_session($session);
468 return OpenILS::Event->new('ROUTE_ITEM',
469 route_to => $hold->pickup_lib, payload => $payload );
472 sub _build_hold_transit {
473 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
474 my $trans = Fieldmapper::action::hold_transit_copy->new;
476 $trans->hold($hold->id);
477 $trans->source($user->home_ou);
478 $trans->dest($hold->pickup_lib);
479 $trans->source_send_time("now");
480 $trans->target_copy($copy->id);
481 $trans->copy_status($copy->status);
483 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
484 my ($stat) = $meth->run( $login_session, $trans, $session );
485 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
486 else { $copy->status(6); } #status in transit
490 sub find_local_hold {
491 my( $class, $session, $copy, $user ) = @_;
492 return _find_local_hold_for_copy($session, $copy, $user);
495 sub _find_local_hold_for_copy {
500 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
502 # first see if this copy has already been selected to fulfill a hold
503 my $hold = $session->request(
504 "open-ils.storage.direct.action.hold_request.search_where",
505 { current_copy => $copy->id, capture_time => undef } )->gather(1);
507 if($hold) {return $hold;}
509 $logger->debug("searching for local hold at org " .
510 $user->home_ou . " and copy " . $copy->id);
512 my $holdid = $session->request(
513 "open-ils.storage.action.hold_request.nearest_hold",
514 $user->home_ou, $copy->id )->gather(1);
516 return (undef, $evt) unless defined $holdid;
518 $logger->debug("Found hold id $holdid while ".
519 "searching nearest hold to " .$user->home_ou);
521 return $apputils->fetch_hold($holdid);
525 __PACKAGE__->register_method(
526 method => "create_hold_transit",
527 api_name => "open-ils.circ.hold_transit.create",
529 Creates a new transit object
532 sub create_hold_transit {
533 my( $self, $client, $login_session, $transit, $session ) = @_;
535 my( $user, $evt ) = $apputils->checkses($login_session);
537 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
541 if($session) { $ses = $session; }
542 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
544 return $ses->request(
545 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
549 sub fetch_open_hold_by_current_copy {
552 my $hold = $apputils->simplereq(
554 'open-ils.storage.direct.action.hold_request.search.atomic',
555 current_copy => $copyid , fulfillment_time => undef );
556 return $hold->[0] if ref($hold);
560 sub fetch_related_holds {
563 return $apputils->simplereq(
565 'open-ils.storage.direct.action.hold_request.search.atomic',
566 current_copy => $copyid , fulfillment_time => undef );
570 __PACKAGE__->register_method (
571 method => "hold_pull_list",
572 api_name => "open-ils.circ.hold_pull_list.retrieve",
574 Returns a list of hold ID's that need to be "pulled"
580 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
581 my( $reqr, $evt ) = $U->checkses($authtoken);
584 my $org = $reqr->ws_ou || $reqr->home_ou;
585 # the perm locaiton shouldn't really matter here since holds
586 # will exist all over and VIEW_HOLDS should be universal
587 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
590 return $U->storagereq(
591 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
592 $org, $limit, $offset ); # XXX change to workstation
595 __PACKAGE__->register_method (
596 method => 'fetch_hold_notify',
597 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
599 Returns a list of hold notification objects based on hold id.
600 @param authtoken The loggin session key
601 @param holdid The id of the hold whose notifications we want to retrieve
602 @return An array of hold notification objects, event on error.
606 sub fetch_hold_notify {
607 my( $self, $conn, $authtoken, $holdid ) = @_;
608 my( $requestor, $evt ) = $U->checkses($authtoken);
611 ($hold, $evt) = $U->fetch_hold($holdid);
613 ($patron, $evt) = $U->fetch_user($hold->usr);
616 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
619 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
620 return $U->storagereq(
621 'open-ils.storage.direct.action.hold_notification.search.hold.atomic', $holdid );
625 __PACKAGE__->register_method (
626 method => 'create_hold_notify',
627 api_name => 'open-ils.circ.hold_notification.create',
629 Creates a new hold notification object
630 @param authtoken The login session key
631 @param notification The hold notification object to create
632 @return ID of the new object on success, Event on error
635 sub create_hold_notify {
636 my( $self, $conn, $authtoken, $notification ) = @_;
637 my( $requestor, $evt ) = $U->checkses($authtoken);
640 ($hold, $evt) = $U->fetch_hold($notification->hold);
642 ($patron, $evt) = $U->fetch_user($hold->usr);
645 # XXX perm depth probably doesn't matter here -- should always be consortium level
646 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
649 # Set the proper notifier
650 $notification->notify_staff($requestor->id);
651 my $id = $U->storagereq(
652 'open-ils.storage.direct.action.hold_notification.create', $notification );
653 return $U->DB_UPDATE_FAILED($notification) unless $id;
654 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
659 __PACKAGE__->register_method(
660 method => 'reset_hold',
661 api_name => 'open-ils.circ.hold.reset',
663 Un-captures and un-targets a hold, essentially returning
664 it to the state it was in directly after it was placed,
665 then attempts to re-target the hold
666 @param authtoken The login session key
667 @param holdid The id of the hold
673 my( $self, $conn, $auth, $holdid ) = @_;
675 my ($hold, $evt) = $U->fetch_hold($holdid);
677 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
679 $evt = $self->_reset_hold($reqr, $hold);
685 my ($self, $reqr, $hold, $session) = @_;
690 $session = $U->start_db_session();
693 $hold->clear_capture_time;
694 $hold->clear_current_copy;
696 return $U->DB_UPDATE_FAILED($hold) unless
698 'open-ils.storage.direct.action.hold_request.update', $hold )->gather(1);
701 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id )->gather(1);
703 $U->commit_db_session($session) unless $x;
708 __PACKAGE__->register_method(
709 method => 'fetch_open_title_holds',
710 api_name => 'open-ils.circ.open_holds.retrieve',
712 Returns a list ids of un-fulfilled holds for a given title id
713 @param authtoken The login session key
714 @param id the id of the item whose holds we want to retrieve
715 @param type The hold type - M, T, V, C
719 sub fetch_open_title_holds {
720 my( $self, $conn, $auth, $id, $type, $org ) = @_;
721 my $e = OpenILS::Utils::Editor->new( authtoken => $auth );
722 return $e->event unless $e->checkauth;
725 $org ||= $e->requestor->ws_ou;
727 # return $e->search_action_hold_request(
728 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
730 # XXX make me return IDs in the future ^--
731 return $e->search_action_hold_request(
732 { target => $id, hold_type => $type, fulfillment_time => undef });
738 __PACKAGE__->register_method(
739 method => 'fetch_captured_holds',
740 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
742 Returns a list ids of un-fulfilled holds for a given title id
743 @param authtoken The login session key
744 @param org The org id of the location in question
747 sub fetch_captured_holds {
748 my( $self, $conn, $auth, $org ) = @_;
750 my $e = OpenILS::Utils::Editor->new(authtoken => $auth);
751 return $e->event unless $e->checkauth;
752 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
754 $org ||= $e->requestor->ws_ou;
756 my $holds = $e->search_action_hold_request(
758 capture_time => { "!=" => undef },
759 current_copy => { "!=" => undef },
760 fulfillment_time => undef,
766 my $stat = $U->copy_status_from_name('on holds shelf');
767 for my $h (@$holds) {
768 my $copy = $e->retrieve_asset_copy($h->current_copy)
770 push( @res, $h ) if $copy->status == $stat->id; # eventually, push IDs here