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);
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);
109 my $resp = $apputils->simplereq(
111 'open-ils.storage.direct.action.hold_request.create', $hold );
114 return OpenSRF::EX::ERROR ("Error creating hold");
121 # makes sure that a user has permission to place the type of requested hold
122 # returns the Perm exception if not allowed, returns undef if all is well
123 sub _check_holds_perm {
124 my($type, $user_id, $org_id) = @_;
128 if($evt = $apputils->check_perms(
129 $user_id, $org_id, "MR_HOLDS")) {
133 } elsif ($type eq "T") {
134 if($evt = $apputils->check_perms(
135 $user_id, $org_id, "TITLE_HOLDS")) {
139 } elsif($type eq "V") {
140 if($evt = $apputils->check_perms(
141 $user_id, $org_id, "VOLUME_HOLDS")) {
145 } elsif($type eq "C") {
146 if($evt = $apputils->check_perms(
147 $user_id, $org_id, "COPY_HOLDS")) {
155 # tests if the given user is allowed to place holds on another's behalf
156 sub _check_request_holds_perm {
159 if(my $evt = $apputils->check_perms(
160 $user_id, $org_id, "REQUEST_HOLDS")) {
165 sub _check_request_holds_override {
168 if(my $evt = $apputils->check_perms(
169 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
175 __PACKAGE__->register_method(
176 method => "retrieve_holds",
177 api_name => "open-ils.circ.holds.retrieve",
179 Retrieves all the holds, with hold transits attached, for the specified
180 user id. The login session is the requestor and if the requestor is
181 different from the user, then the requestor must have VIEW_HOLD permissions.
186 my($self, $client, $login_session, $user_id) = @_;
188 my( $user, $target, $evt ) = $apputils->checkses_requestor(
189 $login_session, $user_id, 'VIEW_HOLD' );
192 my $holds = $apputils->simplereq(
194 "open-ils.storage.direct.action.hold_request.search.atomic",
195 "usr" => $user_id , fulfillment_time => undef, { order_by => "request_time" });
197 for my $hold ( @$holds ) {
199 $apputils->simplereq(
201 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
202 { order_by => 'id desc', limit => 1 }
210 __PACKAGE__->register_method(
211 method => "retrieve_holds_by_pickup_lib",
212 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
214 Retrieves all the holds, with hold transits attached, for the specified
219 sub retrieve_holds_by_pickup_lib {
220 my($self, $client, $login_session, $ou_id) = @_;
222 #FIXME -- put an appropriate permission check here
223 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
224 # $login_session, $user_id, 'VIEW_HOLD' );
225 #return $evt if $evt;
227 my $holds = $apputils->simplereq(
229 "open-ils.storage.direct.action.hold_request.search.atomic",
230 "pickup_lib" => $ou_id , fulfillment_time => undef, { order_by => "request_time" });
232 for my $hold ( @$holds ) {
234 $apputils->simplereq(
236 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
237 { order_by => 'id desc', limit => 1 }
246 __PACKAGE__->register_method(
247 method => "cancel_hold",
248 api_name => "open-ils.circ.hold.cancel",
250 Cancels the specified hold. The login session
251 is the requestor and if the requestor is different from the usr field
252 on the hold, the requestor must have CANCEL_HOLDS permissions.
253 the hold may be either the hold object or the hold id
257 my($self, $client, $login_session, $holdid) = @_;
260 my($user, $evt) = $U->checkses($login_session);
263 ( $hold, $evt ) = $U->fetch_hold($holdid);
266 if($user->id ne $hold->usr) { #am I allowed to cancel this user's hold?
267 if($evt = $apputils->check_perms(
268 $user->id, $user->home_ou, 'CANCEL_HOLDS')) {
273 $logger->activity( "User " . $user->id .
274 " canceling hold $holdid for user " . $hold->usr );
276 return $apputils->simplereq(
278 "open-ils.storage.direct.action.hold_request.delete", $hold );
282 __PACKAGE__->register_method(
283 method => "update_hold",
284 api_name => "open-ils.circ.hold.update",
286 Updates the specified hold. The login session
287 is the requestor and if the requestor is different from the usr field
288 on the hold, the requestor must have UPDATE_HOLDS permissions.
292 my($self, $client, $login_session, $hold) = @_;
294 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
295 $login_session, $hold->usr, 'UPDATE_HOLD' );
298 $logger->activity('User ' + $requestor->id .
299 ' updating hold ' . $hold->id . ' for user ' . $target->id );
301 return $apputils->simplereq(
303 "open-ils.storage.direct.action.hold_request.update", $hold );
307 __PACKAGE__->register_method(
308 method => "retrieve_hold_status",
309 api_name => "open-ils.circ.hold.status.retrieve",
311 Calculates the current status of the hold.
312 the requestor must have VIEW_HOLD permissions if the hold is for a user
313 other than the requestor.
314 Returns -1 on error (for now)
315 Returns 1 for 'waiting for copy to become available'
316 Returns 2 for 'waiting for copy capture'
317 Returns 3 for 'in transit'
318 Returns 4 for 'arrived'
321 sub retrieve_hold_status {
322 my($self, $client, $login_session, $hold_id) = @_;
325 my( $requestor, $target, $hold, $copy, $transit, $evt );
327 ( $hold, $evt ) = $apputils->fetch_hold($hold_id);
330 ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
331 $login_session, $hold->usr, 'VIEW_HOLD' );
334 return 1 unless (defined($hold->current_copy));
336 ( $copy, $evt ) = $apputils->fetch_copy($hold->current_copy);
339 return 4 if ($hold->capture_time and $copy->circ_lib eq $hold->pickup_lib);
341 ( $transit, $evt ) = $apputils->fetch_hold_transit_by_hold( $hold->id );
342 return 4 if(ref($transit) and defined($transit->dest_recv_time) );
344 return 3 if defined($hold->capture_time);
353 __PACKAGE__->register_method(
354 method => "capture_copy",
355 api_name => "open-ils.circ.hold.capture_copy.barcode",
357 Captures a copy to fulfil a hold
358 Params is login session and copy barcode
359 Optional param is 'flesh'. If set, we also return the
360 relevant copy and title
361 login mus have COPY_CHECKIN permissions (since this is essentially
365 # XXX deprecate me XXX
368 my( $self, $client, $login_session, $params ) = @_;
369 my %params = %$params;
370 my $barcode = $params{barcode};
373 my( $user, $target, $copy, $hold, $evt );
375 ( $user, $evt ) = $apputils->checkses($login_session);
378 # am I allowed to checkin a copy?
379 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
382 $logger->info("Capturing copy with barcode $barcode");
384 my $session = $apputils->start_db_session();
386 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
389 $logger->debug("Capturing copy " . $copy->id);
391 ( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
394 warn "Found hold " . $hold->id . "\n";
395 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
397 $hold->current_copy($copy->id);
398 $hold->capture_time("now");
401 my $stat = $session->request(
402 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
403 if(!$stat) { throw OpenSRF::EX::ERROR
404 ("Error updating hold request " . $copy->id); }
406 $copy->status(8); #status on holds shelf
408 # if the staff member capturing this item is not at the pickup lib
409 if( $user->home_ou ne $hold->pickup_lib ) {
410 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
413 $copy->editor($user->id);
414 $copy->edit_date("now");
415 $stat = $session->request(
416 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
417 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
419 my $payload = { hold => $hold };
420 $payload->{copy} = $copy if $params{flesh_copy};
422 if($params{flesh_record}) {
424 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
426 $record = $apputils->record_to_mvr($record);
427 $payload->{record} = $record;
430 $apputils->commit_db_session($session);
432 return OpenILS::Event->new('ROUTE_ITEM',
433 route_to => $hold->pickup_lib, payload => $payload );
436 sub _build_hold_transit {
437 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
438 my $trans = Fieldmapper::action::hold_transit_copy->new;
440 $trans->hold($hold->id);
441 $trans->source($user->home_ou);
442 $trans->dest($hold->pickup_lib);
443 $trans->source_send_time("now");
444 $trans->target_copy($copy->id);
445 $trans->copy_status($copy->status);
447 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
448 my ($stat) = $meth->run( $login_session, $trans, $session );
449 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
450 else { $copy->status(6); } #status in transit
454 sub find_local_hold {
455 my( $class, $session, $copy, $user ) = @_;
456 return _find_local_hold_for_copy($session, $copy, $user);
459 sub _find_local_hold_for_copy {
464 my $evt = OpenILS::Event->new('HOLD_NOT_FOUND');
466 # first see if this copy has already been selected to fulfill a hold
467 my $hold = $session->request(
468 "open-ils.storage.direct.action.hold_request.search_where",
469 { current_copy => $copy->id, capture_time => undef } )->gather(1);
471 if($hold) {return $hold;}
473 $logger->debug("searching for local hold at org " .
474 $user->home_ou . " and copy " . $copy->id);
476 my $holdid = $session->request(
477 "open-ils.storage.action.hold_request.nearest_hold",
478 $user->home_ou, $copy->id )->gather(1);
480 return (undef, $evt) unless defined $holdid;
482 $logger->debug("Found hold id $holdid while ".
483 "searching nearest hold to " .$user->home_ou);
485 return $apputils->fetch_hold($holdid);
489 __PACKAGE__->register_method(
490 method => "create_hold_transit",
491 api_name => "open-ils.circ.hold_transit.create",
493 Creates a new transit object
496 sub create_hold_transit {
497 my( $self, $client, $login_session, $transit, $session ) = @_;
499 my( $user, $evt ) = $apputils->checkses($login_session);
501 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
505 if($session) { $ses = $session; }
506 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
508 return $ses->request(
509 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
513 sub fetch_open_hold_by_current_copy {
516 my $hold = $apputils->simplereq(
518 'open-ils.storage.direct.action.hold_request.search.atomic',
519 current_copy => $copyid , fulfillment_time => undef );
520 return $hold->[0] if ref($hold);
524 sub fetch_related_holds {
527 return $apputils->simplereq(
529 'open-ils.storage.direct.action.hold_request.search.atomic',
530 current_copy => $copyid , fulfillment_time => undef );
534 __PACKAGE__->register_method (
535 method => "hold_pull_list",
536 api_name => "open-ils.circ.hold_pull_list.retrieve",
538 Returns a list of hold ID's that need to be "pulled"
544 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
545 my( $reqr, $evt ) = $U->checkses($authtoken);
548 my $org = $reqr->ws_ou || $reqr->home_ou;
549 # the perm locaiton shouldn't really matter here since holds
550 # will exist all over and VIEW_HOLDS should be universal
551 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
554 return $U->storagereq(
555 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
556 $org, $limit, $offset ); # XXX change to workstation
559 __PACKAGE__->register_method (
560 method => 'fetch_hold_notify',
561 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
563 Returns a list of hold notification objects based on hold id.
564 @param authtoken The loggin session key
565 @param holdid The id of the hold whose notifications we want to retrieve
566 @return An array of hold notification objects, event on error.
570 sub fetch_hold_notify {
571 my( $self, $conn, $authtoken, $holdid ) = @_;
572 my( $requestor, $evt ) = $U->checkses($authtoken);
575 ($hold, $evt) = $U->fetch_hold($holdid);
577 ($patron, $evt) = $U->fetch_user($hold->usr);
580 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
583 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
584 return $U->storagereq(
585 'open-ils.storage.direct.action.hold_notification.search.hold.atomic', $holdid );
589 __PACKAGE__->register_method (
590 method => 'create_hold_notify',
591 api_name => 'open-ils.circ.hold_notification.create',
593 Creates a new hold notification object
594 @param authtoken The login session key
595 @param notification The hold notification object to create
596 @return ID of the new object on success, Event on error
599 sub create_hold_notify {
600 my( $self, $conn, $authtoken, $notification ) = @_;
601 my( $requestor, $evt ) = $U->checkses($authtoken);
604 ($hold, $evt) = $U->fetch_hold($notification->hold);
606 ($patron, $evt) = $U->fetch_user($hold->usr);
609 # XXX perm depth probably doesn't matter here -- should always be consortium level
610 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
613 # Set the proper notifier
614 $notification->notify_staff($requestor->id);
615 my $id = $U->storagereq(
616 'open-ils.storage.direct.action.hold_notification.create', $notification );
617 return $U->DB_UPDATE_FAILED($notification) unless $id;
618 $logger->info("User ".$requestor->id." successfully created new hold notification $id");