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")) {
174 __PACKAGE__->register_method(
175 method => "retrieve_holds_by_id",
176 api_name => "open-ils.circ.holds.retrieve_by_id",
178 Retrieve the hold, with hold transits attached, for the specified id
179 The login session is the requestor and if the requestor is
180 different from the user, then the requestor must have VIEW_HOLD permissions.
184 sub retrieve_holds_by_id {
185 my($self, $client, $login_session, $hold_id) = @_;
188 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
189 # $login_session, $user_id, 'VIEW_HOLD' );
190 #return $evt if $evt;
192 my $holds = $apputils->simplereq(
194 "open-ils.storage.direct.action.hold_request.search.atomic",
195 "id" => $hold_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 }
211 __PACKAGE__->register_method(
212 method => "retrieve_holds",
213 api_name => "open-ils.circ.holds.retrieve",
215 Retrieves all the holds, with hold transits attached, for the specified
216 user id. The login session is the requestor and if the requestor is
217 different from the user, then the requestor must have VIEW_HOLD permissions.
222 my($self, $client, $login_session, $user_id) = @_;
224 my( $user, $target, $evt ) = $apputils->checkses_requestor(
225 $login_session, $user_id, 'VIEW_HOLD' );
228 my $holds = $apputils->simplereq(
230 "open-ils.storage.direct.action.hold_request.search.atomic",
231 "usr" => $user_id , fulfillment_time => undef, { order_by => "request_time" });
233 for my $hold ( @$holds ) {
235 $apputils->simplereq(
237 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
238 { order_by => 'id desc', limit => 1 }
246 __PACKAGE__->register_method(
247 method => "retrieve_holds_by_pickup_lib",
248 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
250 Retrieves all the holds, with hold transits attached, for the specified
255 sub retrieve_holds_by_pickup_lib {
256 my($self, $client, $login_session, $ou_id) = @_;
258 #FIXME -- put an appropriate permission check here
259 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
260 # $login_session, $user_id, 'VIEW_HOLD' );
261 #return $evt if $evt;
263 my $holds = $apputils->simplereq(
265 "open-ils.storage.direct.action.hold_request.search.atomic",
266 "pickup_lib" => $ou_id , fulfillment_time => undef, { order_by => "request_time" });
268 for my $hold ( @$holds ) {
270 $apputils->simplereq(
272 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
273 { order_by => 'id desc', limit => 1 }
282 __PACKAGE__->register_method(
283 method => "cancel_hold",
284 api_name => "open-ils.circ.hold.cancel",
286 Cancels 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 CANCEL_HOLDS permissions.
289 the hold may be either the hold object or the hold id
293 my($self, $client, $login_session, $holdid) = @_;
296 my($user, $evt) = $U->checkses($login_session);
299 ( $hold, $evt ) = $U->fetch_hold($holdid);
302 if($user->id ne $hold->usr) { #am I allowed to cancel this user's hold?
303 if($evt = $apputils->check_perms(
304 $user->id, $user->home_ou, 'CANCEL_HOLDS')) {
309 $logger->activity( "User " . $user->id .
310 " canceling hold $holdid for user " . $hold->usr );
312 return $apputils->simplereq(
314 "open-ils.storage.direct.action.hold_request.delete", $hold );
318 __PACKAGE__->register_method(
319 method => "update_hold",
320 api_name => "open-ils.circ.hold.update",
322 Updates the specified hold. The login session
323 is the requestor and if the requestor is different from the usr field
324 on the hold, the requestor must have UPDATE_HOLDS permissions.
328 my($self, $client, $login_session, $hold) = @_;
330 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
331 $login_session, $hold->usr, 'UPDATE_HOLD' );
334 $logger->activity('User ' . $requestor->id .
335 ' updating hold ' . $hold->id . ' for user ' . $target->id );
337 return $U->storagereq(
338 "open-ils.storage.direct.action.hold_request.update", $hold );
342 __PACKAGE__->register_method(
343 method => "retrieve_hold_status",
344 api_name => "open-ils.circ.hold.status.retrieve",
346 Calculates the current status of the hold.
347 the requestor must have VIEW_HOLD permissions if the hold is for a user
348 other than the requestor.
349 Returns -1 on error (for now)
350 Returns 1 for 'waiting for copy to become available'
351 Returns 2 for 'waiting for copy capture'
352 Returns 3 for 'in transit'
353 Returns 4 for 'arrived'
356 sub retrieve_hold_status {
357 my($self, $client, $login_session, $hold_id) = @_;
360 my( $requestor, $target, $hold, $copy, $transit, $evt );
362 ( $hold, $evt ) = $apputils->fetch_hold($hold_id);
365 ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
366 $login_session, $hold->usr, 'VIEW_HOLD' );
369 return 1 unless (defined($hold->current_copy));
371 ( $copy, $evt ) = $apputils->fetch_copy($hold->current_copy);
374 return 4 if ($hold->capture_time and $copy->circ_lib eq $hold->pickup_lib);
376 ( $transit, $evt ) = $apputils->fetch_hold_transit_by_hold( $hold->id );
377 return 4 if(ref($transit) and defined($transit->dest_recv_time) );
379 return 3 if defined($hold->capture_time);
388 __PACKAGE__->register_method(
389 method => "capture_copy",
390 api_name => "open-ils.circ.hold.capture_copy.barcode",
392 Captures a copy to fulfil a hold
393 Params is login session and copy barcode
394 Optional param is 'flesh'. If set, we also return the
395 relevant copy and title
396 login mus have COPY_CHECKIN permissions (since this is essentially
400 # XXX deprecate me XXX
403 my( $self, $client, $login_session, $params ) = @_;
404 my %params = %$params;
405 my $barcode = $params{barcode};
408 my( $user, $target, $copy, $hold, $evt );
410 ( $user, $evt ) = $apputils->checkses($login_session);
413 # am I allowed to checkin a copy?
414 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
417 $logger->info("Capturing copy with barcode $barcode");
419 my $session = $apputils->start_db_session();
421 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
424 $logger->debug("Capturing copy " . $copy->id);
426 ( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
429 warn "Found hold " . $hold->id . "\n";
430 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
432 $hold->current_copy($copy->id);
433 $hold->capture_time("now");
436 my $stat = $session->request(
437 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
438 if(!$stat) { throw OpenSRF::EX::ERROR
439 ("Error updating hold request " . $copy->id); }
441 $copy->status(8); #status on holds shelf
443 # if the staff member capturing this item is not at the pickup lib
444 if( $user->home_ou ne $hold->pickup_lib ) {
445 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
448 $copy->editor($user->id);
449 $copy->edit_date("now");
450 $stat = $session->request(
451 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
452 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
454 my $payload = { hold => $hold };
455 $payload->{copy} = $copy if $params{flesh_copy};
457 if($params{flesh_record}) {
459 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
461 $record = $apputils->record_to_mvr($record);
462 $payload->{record} = $record;
465 $apputils->commit_db_session($session);
467 return OpenILS::Event->new('ROUTE_ITEM',
468 route_to => $hold->pickup_lib, payload => $payload );
471 sub _build_hold_transit {
472 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
473 my $trans = Fieldmapper::action::hold_transit_copy->new;
475 $trans->hold($hold->id);
476 $trans->source($user->home_ou);
477 $trans->dest($hold->pickup_lib);
478 $trans->source_send_time("now");
479 $trans->target_copy($copy->id);
480 $trans->copy_status($copy->status);
482 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
483 my ($stat) = $meth->run( $login_session, $trans, $session );
484 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
485 else { $copy->status(6); } #status in transit
489 sub find_local_hold {
490 my( $class, $session, $copy, $user ) = @_;
491 return _find_local_hold_for_copy($session, $copy, $user);
494 sub _find_local_hold_for_copy {
499 my $evt = OpenILS::Event->new('HOLD_NOT_FOUND');
501 # first see if this copy has already been selected to fulfill a hold
502 my $hold = $session->request(
503 "open-ils.storage.direct.action.hold_request.search_where",
504 { current_copy => $copy->id, capture_time => undef } )->gather(1);
506 if($hold) {return $hold;}
508 $logger->debug("searching for local hold at org " .
509 $user->home_ou . " and copy " . $copy->id);
511 my $holdid = $session->request(
512 "open-ils.storage.action.hold_request.nearest_hold",
513 $user->home_ou, $copy->id )->gather(1);
515 return (undef, $evt) unless defined $holdid;
517 $logger->debug("Found hold id $holdid while ".
518 "searching nearest hold to " .$user->home_ou);
520 return $apputils->fetch_hold($holdid);
524 __PACKAGE__->register_method(
525 method => "create_hold_transit",
526 api_name => "open-ils.circ.hold_transit.create",
528 Creates a new transit object
531 sub create_hold_transit {
532 my( $self, $client, $login_session, $transit, $session ) = @_;
534 my( $user, $evt ) = $apputils->checkses($login_session);
536 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
540 if($session) { $ses = $session; }
541 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
543 return $ses->request(
544 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
548 sub fetch_open_hold_by_current_copy {
551 my $hold = $apputils->simplereq(
553 'open-ils.storage.direct.action.hold_request.search.atomic',
554 current_copy => $copyid , fulfillment_time => undef );
555 return $hold->[0] if ref($hold);
559 sub fetch_related_holds {
562 return $apputils->simplereq(
564 'open-ils.storage.direct.action.hold_request.search.atomic',
565 current_copy => $copyid , fulfillment_time => undef );
569 __PACKAGE__->register_method (
570 method => "hold_pull_list",
571 api_name => "open-ils.circ.hold_pull_list.retrieve",
573 Returns a list of hold ID's that need to be "pulled"
579 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
580 my( $reqr, $evt ) = $U->checkses($authtoken);
583 my $org = $reqr->ws_ou || $reqr->home_ou;
584 # the perm locaiton shouldn't really matter here since holds
585 # will exist all over and VIEW_HOLDS should be universal
586 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
589 return $U->storagereq(
590 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
591 $org, $limit, $offset ); # XXX change to workstation
594 __PACKAGE__->register_method (
595 method => 'fetch_hold_notify',
596 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
598 Returns a list of hold notification objects based on hold id.
599 @param authtoken The loggin session key
600 @param holdid The id of the hold whose notifications we want to retrieve
601 @return An array of hold notification objects, event on error.
605 sub fetch_hold_notify {
606 my( $self, $conn, $authtoken, $holdid ) = @_;
607 my( $requestor, $evt ) = $U->checkses($authtoken);
610 ($hold, $evt) = $U->fetch_hold($holdid);
612 ($patron, $evt) = $U->fetch_user($hold->usr);
615 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
618 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
619 return $U->storagereq(
620 'open-ils.storage.direct.action.hold_notification.search.hold.atomic', $holdid );
624 __PACKAGE__->register_method (
625 method => 'create_hold_notify',
626 api_name => 'open-ils.circ.hold_notification.create',
628 Creates a new hold notification object
629 @param authtoken The login session key
630 @param notification The hold notification object to create
631 @return ID of the new object on success, Event on error
634 sub create_hold_notify {
635 my( $self, $conn, $authtoken, $notification ) = @_;
636 my( $requestor, $evt ) = $U->checkses($authtoken);
639 ($hold, $evt) = $U->fetch_hold($notification->hold);
641 ($patron, $evt) = $U->fetch_user($hold->usr);
644 # XXX perm depth probably doesn't matter here -- should always be consortium level
645 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
648 # Set the proper notifier
649 $notification->notify_staff($requestor->id);
650 my $id = $U->storagereq(
651 'open-ils.storage.direct.action.hold_notification.create', $notification );
652 return $U->DB_UPDATE_FAILED($notification) unless $id;
653 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
658 __PACKAGE__->register_method(
659 method => 'reset_hold',
660 api_name => 'open-ils.circ.hold.reset',
662 Un-captures and un-targets a hold, essentially returning
663 it to the state it was in directly after it was placed,
664 then attempts to re-target the hold
665 @param authtoken The login session key
666 @param holdid The id of the hold
672 my( $self, $conn, $auth, $holdid ) = @_;
674 my ($hold, $evt) = $U->fetch_hold($holdid);
676 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
678 $evt = $self->_reset_hold($reqr, $hold);
684 my ($self, $reqr, $hold, $session) = @_;
689 $session = $U->start_db_session();
692 $hold->clear_capture_time;
693 $hold->clear_current_copy;
695 return $U->DB_UPDATE_FAILED($hold) unless
697 'open-ils.storage.direct.action.hold_request.update', $hold )->gather(1);
700 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id )->gather(1);
702 $U->commit_db_session($session) unless $x;