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;
23 use OpenSRF::EX qw(:try);
26 use OpenSRF::Utils::Logger qw(:logger);
27 use OpenILS::Utils::Editor;
29 my $apputils = "OpenILS::Application::AppUtils";
34 __PACKAGE__->register_method(
35 method => "create_hold",
36 api_name => "open-ils.circ.holds.create",
38 Create a new hold for an item. From a permissions perspective,
39 the login session is used as the 'requestor' of the hold.
40 The hold recipient is determined by the 'usr' setting within
43 First we verify the requestion has holds request permissions.
44 Then we verify that the recipient is allowed to make the given hold.
45 If not, we see if the requestor has "override" capabilities. If not,
46 a permission exception is returned. If permissions allow, we cycle
47 through the set of holds objects and create.
49 If the recipient does not have permission to place multiple holds
50 on a single title and said operation is attempted, a permission
55 my( $self, $client, $login_session, @holds) = @_;
57 if(!@holds){return 0;}
58 my( $user, $evt ) = $apputils->checkses($login_session);
62 if(ref($holds[0]) eq 'ARRAY') {
64 } else { $holds = [ @holds ]; }
66 $logger->debug("Iterating over holds requests...");
68 for my $hold (@$holds) {
71 my $type = $hold->hold_type;
73 $logger->activity("User " . $user->id .
74 " creating new hold of type $type for user " . $hold->usr);
77 if($user->id ne $hold->usr) {
78 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
88 # am I allowed to place holds for this user?
89 if($hold->requestor ne $hold->usr) {
90 $perm = _check_request_holds_perm($user->id, $user->home_ou);
91 if($perm) { return $perm; }
94 # is this user allowed to have holds of this type?
95 $perm = _check_holds_perm($type, $hold->usr, $recipient->home_ou);
97 #if there is a requestor, see if the requestor has override privelages
98 if($hold->requestor ne $hold->usr) {
99 $perm = _check_request_holds_override($user->id, $user->home_ou);
100 if($perm) {return $perm;}
107 #enforce the fact that the login is the one requesting the hold
108 $hold->requestor($user->id);
109 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
111 my $resp = $apputils->simplereq(
113 'open-ils.storage.direct.action.hold_request.create', $hold );
116 return OpenSRF::EX::ERROR ("Error creating hold");
123 # makes sure that a user has permission to place the type of requested hold
124 # returns the Perm exception if not allowed, returns undef if all is well
125 sub _check_holds_perm {
126 my($type, $user_id, $org_id) = @_;
130 if($evt = $apputils->check_perms(
131 $user_id, $org_id, "MR_HOLDS")) {
135 } elsif ($type eq "T") {
136 if($evt = $apputils->check_perms(
137 $user_id, $org_id, "TITLE_HOLDS")) {
141 } elsif($type eq "V") {
142 if($evt = $apputils->check_perms(
143 $user_id, $org_id, "VOLUME_HOLDS")) {
147 } elsif($type eq "C") {
148 if($evt = $apputils->check_perms(
149 $user_id, $org_id, "COPY_HOLDS")) {
157 # tests if the given user is allowed to place holds on another's behalf
158 sub _check_request_holds_perm {
161 if(my $evt = $apputils->check_perms(
162 $user_id, $org_id, "REQUEST_HOLDS")) {
167 sub _check_request_holds_override {
170 if(my $evt = $apputils->check_perms(
171 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
176 __PACKAGE__->register_method(
177 method => "retrieve_holds_by_id",
178 api_name => "open-ils.circ.holds.retrieve_by_id",
180 Retrieve the hold, with hold transits attached, for the specified id
181 The login session is the requestor and if the requestor is
182 different from the user, then the requestor must have VIEW_HOLD permissions.
186 sub retrieve_holds_by_id {
187 my($self, $client, $login_session, $hold_id) = @_;
190 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
191 # $login_session, $user_id, 'VIEW_HOLD' );
192 #return $evt if $evt;
194 my $holds = $apputils->simplereq(
196 "open-ils.storage.direct.action.hold_request.search.atomic",
197 "id" => $hold_id , fulfillment_time => undef, { order_by => "request_time" });
199 for my $hold ( @$holds ) {
201 $apputils->simplereq(
203 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
204 { order_by => 'id desc', limit => 1 }
213 __PACKAGE__->register_method(
214 method => "retrieve_holds",
215 api_name => "open-ils.circ.holds.retrieve",
217 Retrieves all the holds, with hold transits attached, for the specified
218 user id. The login session is the requestor and if the requestor is
219 different from the user, then the requestor must have VIEW_HOLD permissions.
224 my($self, $client, $login_session, $user_id) = @_;
226 my( $user, $target, $evt ) = $apputils->checkses_requestor(
227 $login_session, $user_id, 'VIEW_HOLD' );
230 my $holds = $apputils->simplereq(
232 "open-ils.storage.direct.action.hold_request.search.atomic",
233 "usr" => $user_id , fulfillment_time => undef, { order_by => "request_time" });
235 for my $hold ( @$holds ) {
237 $apputils->simplereq(
239 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
240 { order_by => 'id desc', limit => 1 }
248 __PACKAGE__->register_method(
249 method => "retrieve_holds_by_pickup_lib",
250 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
252 Retrieves all the holds, with hold transits attached, for the specified
257 sub retrieve_holds_by_pickup_lib {
258 my($self, $client, $login_session, $ou_id) = @_;
260 #FIXME -- put an appropriate permission check here
261 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
262 # $login_session, $user_id, 'VIEW_HOLD' );
263 #return $evt if $evt;
265 my $holds = $apputils->simplereq(
267 "open-ils.storage.direct.action.hold_request.search.atomic",
268 "pickup_lib" => $ou_id , fulfillment_time => undef, { order_by => "request_time" });
270 for my $hold ( @$holds ) {
272 $apputils->simplereq(
274 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
275 { order_by => 'id desc', limit => 1 }
284 __PACKAGE__->register_method(
285 method => "cancel_hold",
286 api_name => "open-ils.circ.hold.cancel",
288 Cancels the specified hold. The login session
289 is the requestor and if the requestor is different from the usr field
290 on the hold, the requestor must have CANCEL_HOLDS permissions.
291 the hold may be either the hold object or the hold id
295 my($self, $client, $login_session, $holdid) = @_;
298 my($user, $evt) = $U->checkses($login_session);
301 ( $hold, $evt ) = $U->fetch_hold($holdid);
304 if($user->id ne $hold->usr) { #am I allowed to cancel this user's hold?
305 if($evt = $apputils->check_perms(
306 $user->id, $user->home_ou, 'CANCEL_HOLDS')) {
311 $logger->activity( "User " . $user->id .
312 " canceling hold $holdid for user " . $hold->usr );
314 return $apputils->simplereq(
316 "open-ils.storage.direct.action.hold_request.delete", $hold );
320 __PACKAGE__->register_method(
321 method => "update_hold",
322 api_name => "open-ils.circ.hold.update",
324 Updates the specified hold. The login session
325 is the requestor and if the requestor is different from the usr field
326 on the hold, the requestor must have UPDATE_HOLDS permissions.
330 my($self, $client, $login_session, $hold) = @_;
332 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
333 $login_session, $hold->usr, 'UPDATE_HOLD' );
336 $logger->activity('User ' . $requestor->id .
337 ' updating hold ' . $hold->id . ' for user ' . $target->id );
339 return $U->storagereq(
340 "open-ils.storage.direct.action.hold_request.update", $hold );
344 __PACKAGE__->register_method(
345 method => "retrieve_hold_status",
346 api_name => "open-ils.circ.hold.status.retrieve",
348 Calculates the current status of the hold.
349 the requestor must have VIEW_HOLD permissions if the hold is for a user
350 other than the requestor.
351 Returns -1 on error (for now)
352 Returns 1 for 'waiting for copy to become available'
353 Returns 2 for 'waiting for copy capture'
354 Returns 3 for 'in transit'
355 Returns 4 for 'arrived'
358 sub retrieve_hold_status {
359 my($self, $client, $login_session, $hold_id) = @_;
362 my( $requestor, $target, $hold, $copy, $transit, $evt );
364 ( $hold, $evt ) = $apputils->fetch_hold($hold_id);
367 ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
368 $login_session, $hold->usr, 'VIEW_HOLD' );
371 return 1 unless (defined($hold->current_copy));
373 ( $copy, $evt ) = $apputils->fetch_copy($hold->current_copy);
376 return 4 if ($hold->capture_time and $copy->circ_lib eq $hold->pickup_lib);
378 ( $transit, $evt ) = $apputils->fetch_hold_transit_by_hold( $hold->id );
379 return 4 if(ref($transit) and defined($transit->dest_recv_time) );
381 return 3 if defined($hold->capture_time);
390 __PACKAGE__->register_method(
391 method => "capture_copy",
392 api_name => "open-ils.circ.hold.capture_copy.barcode",
394 Captures a copy to fulfil a hold
395 Params is login session and copy barcode
396 Optional param is 'flesh'. If set, we also return the
397 relevant copy and title
398 login mus have COPY_CHECKIN permissions (since this is essentially
402 # XXX deprecate me XXX
405 my( $self, $client, $login_session, $params ) = @_;
406 my %params = %$params;
407 my $barcode = $params{barcode};
410 my( $user, $target, $copy, $hold, $evt );
412 ( $user, $evt ) = $apputils->checkses($login_session);
415 # am I allowed to checkin a copy?
416 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
419 $logger->info("Capturing copy with barcode $barcode");
421 my $session = $apputils->start_db_session();
423 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
426 $logger->debug("Capturing copy " . $copy->id);
428 ( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
431 warn "Found hold " . $hold->id . "\n";
432 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
434 $hold->current_copy($copy->id);
435 $hold->capture_time("now");
438 my $stat = $session->request(
439 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
440 if(!$stat) { throw OpenSRF::EX::ERROR
441 ("Error updating hold request " . $copy->id); }
443 $copy->status(8); #status on holds shelf
445 # if the staff member capturing this item is not at the pickup lib
446 if( $user->home_ou ne $hold->pickup_lib ) {
447 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
450 $copy->editor($user->id);
451 $copy->edit_date("now");
452 $stat = $session->request(
453 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
454 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
456 my $payload = { hold => $hold };
457 $payload->{copy} = $copy if $params{flesh_copy};
459 if($params{flesh_record}) {
461 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
463 $record = $apputils->record_to_mvr($record);
464 $payload->{record} = $record;
467 $apputils->commit_db_session($session);
469 return OpenILS::Event->new('ROUTE_ITEM',
470 route_to => $hold->pickup_lib, payload => $payload );
473 sub _build_hold_transit {
474 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
475 my $trans = Fieldmapper::action::hold_transit_copy->new;
477 $trans->hold($hold->id);
478 $trans->source($user->home_ou);
479 $trans->dest($hold->pickup_lib);
480 $trans->source_send_time("now");
481 $trans->target_copy($copy->id);
482 $trans->copy_status($copy->status);
484 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
485 my ($stat) = $meth->run( $login_session, $trans, $session );
486 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
487 else { $copy->status(6); } #status in transit
491 sub find_local_hold {
492 my( $class, $session, $copy, $user ) = @_;
493 return _find_local_hold_for_copy($session, $copy, $user);
496 sub _find_local_hold_for_copy {
501 my $evt = OpenILS::Event->new('HOLD_NOT_FOUND');
503 # first see if this copy has already been selected to fulfill a hold
504 my $hold = $session->request(
505 "open-ils.storage.direct.action.hold_request.search_where",
506 { current_copy => $copy->id, capture_time => undef } )->gather(1);
508 if($hold) {return $hold;}
510 $logger->debug("searching for local hold at org " .
511 $user->home_ou . " and copy " . $copy->id);
513 my $holdid = $session->request(
514 "open-ils.storage.action.hold_request.nearest_hold",
515 $user->home_ou, $copy->id )->gather(1);
517 return (undef, $evt) unless defined $holdid;
519 $logger->debug("Found hold id $holdid while ".
520 "searching nearest hold to " .$user->home_ou);
522 return $apputils->fetch_hold($holdid);
526 __PACKAGE__->register_method(
527 method => "create_hold_transit",
528 api_name => "open-ils.circ.hold_transit.create",
530 Creates a new transit object
533 sub create_hold_transit {
534 my( $self, $client, $login_session, $transit, $session ) = @_;
536 my( $user, $evt ) = $apputils->checkses($login_session);
538 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
542 if($session) { $ses = $session; }
543 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
545 return $ses->request(
546 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
550 sub fetch_open_hold_by_current_copy {
553 my $hold = $apputils->simplereq(
555 'open-ils.storage.direct.action.hold_request.search.atomic',
556 current_copy => $copyid , fulfillment_time => undef );
557 return $hold->[0] if ref($hold);
561 sub fetch_related_holds {
564 return $apputils->simplereq(
566 'open-ils.storage.direct.action.hold_request.search.atomic',
567 current_copy => $copyid , fulfillment_time => undef );
571 __PACKAGE__->register_method (
572 method => "hold_pull_list",
573 api_name => "open-ils.circ.hold_pull_list.retrieve",
575 Returns a list of hold ID's that need to be "pulled"
581 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
582 my( $reqr, $evt ) = $U->checkses($authtoken);
585 my $org = $reqr->ws_ou || $reqr->home_ou;
586 # the perm locaiton shouldn't really matter here since holds
587 # will exist all over and VIEW_HOLDS should be universal
588 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
591 return $U->storagereq(
592 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
593 $org, $limit, $offset ); # XXX change to workstation
596 __PACKAGE__->register_method (
597 method => 'fetch_hold_notify',
598 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
600 Returns a list of hold notification objects based on hold id.
601 @param authtoken The loggin session key
602 @param holdid The id of the hold whose notifications we want to retrieve
603 @return An array of hold notification objects, event on error.
607 sub fetch_hold_notify {
608 my( $self, $conn, $authtoken, $holdid ) = @_;
609 my( $requestor, $evt ) = $U->checkses($authtoken);
612 ($hold, $evt) = $U->fetch_hold($holdid);
614 ($patron, $evt) = $U->fetch_user($hold->usr);
617 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
620 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
621 return $U->storagereq(
622 'open-ils.storage.direct.action.hold_notification.search.hold.atomic', $holdid );
626 __PACKAGE__->register_method (
627 method => 'create_hold_notify',
628 api_name => 'open-ils.circ.hold_notification.create',
630 Creates a new hold notification object
631 @param authtoken The login session key
632 @param notification The hold notification object to create
633 @return ID of the new object on success, Event on error
636 sub create_hold_notify {
637 my( $self, $conn, $authtoken, $notification ) = @_;
638 my( $requestor, $evt ) = $U->checkses($authtoken);
641 ($hold, $evt) = $U->fetch_hold($notification->hold);
643 ($patron, $evt) = $U->fetch_user($hold->usr);
646 # XXX perm depth probably doesn't matter here -- should always be consortium level
647 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
650 # Set the proper notifier
651 $notification->notify_staff($requestor->id);
652 my $id = $U->storagereq(
653 'open-ils.storage.direct.action.hold_notification.create', $notification );
654 return $U->DB_UPDATE_FAILED($notification) unless $id;
655 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
660 __PACKAGE__->register_method(
661 method => 'reset_hold',
662 api_name => 'open-ils.circ.hold.reset',
664 Un-captures and un-targets a hold, essentially returning
665 it to the state it was in directly after it was placed,
666 then attempts to re-target the hold
667 @param authtoken The login session key
668 @param holdid The id of the hold
674 my( $self, $conn, $auth, $holdid ) = @_;
676 my ($hold, $evt) = $U->fetch_hold($holdid);
678 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
680 $evt = $self->_reset_hold($reqr, $hold);
686 my ($self, $reqr, $hold, $session) = @_;
691 $session = $U->start_db_session();
694 $hold->clear_capture_time;
695 $hold->clear_current_copy;
697 return $U->DB_UPDATE_FAILED($hold) unless
699 'open-ils.storage.direct.action.hold_request.update', $hold )->gather(1);
702 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id )->gather(1);
704 $U->commit_db_session($session) unless $x;
709 __PACKAGE__->register_method(
710 method => 'fetch_open_title_holds',
711 api_name => 'open-ils.circ.open_holds.retrieve',
713 Returns a list ids of un-fulfilled holds for a given title id
714 @param authtoken The login session key
715 @param id the id of the item whose holds we want to retrieve
716 @param type The hold type - M, T, V, C
720 sub fetch_open_title_holds {
721 my( $self, $conn, $auth, $id, $type, $org ) = @_;
722 my $e = OpenILS::Utils::Editor->new( authtoken => $auth );
723 return $e->event unless $e->checkauth;
726 $org ||= $e->requestor->ws_ou;
728 # return $e->search_action_hold_request(
729 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
731 # XXX make me return IDs in the future ^--
732 return $e->search_action_hold_request(
733 { target => $id, hold_type => $type, fulfillment_time => undef });
739 __PACKAGE__->register_method(
740 method => 'fetch_captured_holds',
741 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
743 Returns a list ids of un-fulfilled holds for a given title id
744 @param authtoken The login session key
745 @param org The org id of the location in question
748 sub fetch_captured_holds {
749 my( $self, $conn, $auth, $org ) = @_;
751 my $e = OpenILS::Utils::Editor->new(authtoken => $auth);
752 return $e->event unless $e->checkauth;
753 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
755 $org ||= $e->requestor->ws_ou;
757 my $holds = $e->search_action_hold_request(
759 capture_time => { "!=" => undef },
760 current_copy => { "!=" => undef },
761 fulfillment_time => undef,
767 my $stat = $U->copy_status_from_name('on holds shelf');
768 for my $h (@$holds) {
769 my $copy = $e->retrieve_asset_copy($h->current_copy)
771 push( @res, $h ) if $copy->status == $stat->id; # eventually, push IDs here