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 $U->storagereq(
302 "open-ils.storage.direct.action.hold_request.update", $hold );
306 __PACKAGE__->register_method(
307 method => "retrieve_hold_status",
308 api_name => "open-ils.circ.hold.status.retrieve",
310 Calculates the current status of the hold.
311 the requestor must have VIEW_HOLD permissions if the hold is for a user
312 other than the requestor.
313 Returns -1 on error (for now)
314 Returns 1 for 'waiting for copy to become available'
315 Returns 2 for 'waiting for copy capture'
316 Returns 3 for 'in transit'
317 Returns 4 for 'arrived'
320 sub retrieve_hold_status {
321 my($self, $client, $login_session, $hold_id) = @_;
324 my( $requestor, $target, $hold, $copy, $transit, $evt );
326 ( $hold, $evt ) = $apputils->fetch_hold($hold_id);
329 ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
330 $login_session, $hold->usr, 'VIEW_HOLD' );
333 return 1 unless (defined($hold->current_copy));
335 ( $copy, $evt ) = $apputils->fetch_copy($hold->current_copy);
338 return 4 if ($hold->capture_time and $copy->circ_lib eq $hold->pickup_lib);
340 ( $transit, $evt ) = $apputils->fetch_hold_transit_by_hold( $hold->id );
341 return 4 if(ref($transit) and defined($transit->dest_recv_time) );
343 return 3 if defined($hold->capture_time);
352 __PACKAGE__->register_method(
353 method => "capture_copy",
354 api_name => "open-ils.circ.hold.capture_copy.barcode",
356 Captures a copy to fulfil a hold
357 Params is login session and copy barcode
358 Optional param is 'flesh'. If set, we also return the
359 relevant copy and title
360 login mus have COPY_CHECKIN permissions (since this is essentially
364 # XXX deprecate me XXX
367 my( $self, $client, $login_session, $params ) = @_;
368 my %params = %$params;
369 my $barcode = $params{barcode};
372 my( $user, $target, $copy, $hold, $evt );
374 ( $user, $evt ) = $apputils->checkses($login_session);
377 # am I allowed to checkin a copy?
378 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
381 $logger->info("Capturing copy with barcode $barcode");
383 my $session = $apputils->start_db_session();
385 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
388 $logger->debug("Capturing copy " . $copy->id);
390 ( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
393 warn "Found hold " . $hold->id . "\n";
394 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
396 $hold->current_copy($copy->id);
397 $hold->capture_time("now");
400 my $stat = $session->request(
401 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
402 if(!$stat) { throw OpenSRF::EX::ERROR
403 ("Error updating hold request " . $copy->id); }
405 $copy->status(8); #status on holds shelf
407 # if the staff member capturing this item is not at the pickup lib
408 if( $user->home_ou ne $hold->pickup_lib ) {
409 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
412 $copy->editor($user->id);
413 $copy->edit_date("now");
414 $stat = $session->request(
415 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
416 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
418 my $payload = { hold => $hold };
419 $payload->{copy} = $copy if $params{flesh_copy};
421 if($params{flesh_record}) {
423 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
425 $record = $apputils->record_to_mvr($record);
426 $payload->{record} = $record;
429 $apputils->commit_db_session($session);
431 return OpenILS::Event->new('ROUTE_ITEM',
432 route_to => $hold->pickup_lib, payload => $payload );
435 sub _build_hold_transit {
436 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
437 my $trans = Fieldmapper::action::hold_transit_copy->new;
439 $trans->hold($hold->id);
440 $trans->source($user->home_ou);
441 $trans->dest($hold->pickup_lib);
442 $trans->source_send_time("now");
443 $trans->target_copy($copy->id);
444 $trans->copy_status($copy->status);
446 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
447 my ($stat) = $meth->run( $login_session, $trans, $session );
448 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
449 else { $copy->status(6); } #status in transit
453 sub find_local_hold {
454 my( $class, $session, $copy, $user ) = @_;
455 return _find_local_hold_for_copy($session, $copy, $user);
458 sub _find_local_hold_for_copy {
463 my $evt = OpenILS::Event->new('HOLD_NOT_FOUND');
465 # first see if this copy has already been selected to fulfill a hold
466 my $hold = $session->request(
467 "open-ils.storage.direct.action.hold_request.search_where",
468 { current_copy => $copy->id, capture_time => undef } )->gather(1);
470 if($hold) {return $hold;}
472 $logger->debug("searching for local hold at org " .
473 $user->home_ou . " and copy " . $copy->id);
475 my $holdid = $session->request(
476 "open-ils.storage.action.hold_request.nearest_hold",
477 $user->home_ou, $copy->id )->gather(1);
479 return (undef, $evt) unless defined $holdid;
481 $logger->debug("Found hold id $holdid while ".
482 "searching nearest hold to " .$user->home_ou);
484 return $apputils->fetch_hold($holdid);
488 __PACKAGE__->register_method(
489 method => "create_hold_transit",
490 api_name => "open-ils.circ.hold_transit.create",
492 Creates a new transit object
495 sub create_hold_transit {
496 my( $self, $client, $login_session, $transit, $session ) = @_;
498 my( $user, $evt ) = $apputils->checkses($login_session);
500 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
504 if($session) { $ses = $session; }
505 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
507 return $ses->request(
508 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
512 sub fetch_open_hold_by_current_copy {
515 my $hold = $apputils->simplereq(
517 'open-ils.storage.direct.action.hold_request.search.atomic',
518 current_copy => $copyid , fulfillment_time => undef );
519 return $hold->[0] if ref($hold);
523 sub fetch_related_holds {
526 return $apputils->simplereq(
528 'open-ils.storage.direct.action.hold_request.search.atomic',
529 current_copy => $copyid , fulfillment_time => undef );
533 __PACKAGE__->register_method (
534 method => "hold_pull_list",
535 api_name => "open-ils.circ.hold_pull_list.retrieve",
537 Returns a list of hold ID's that need to be "pulled"
543 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
544 my( $reqr, $evt ) = $U->checkses($authtoken);
547 my $org = $reqr->ws_ou || $reqr->home_ou;
548 # the perm locaiton shouldn't really matter here since holds
549 # will exist all over and VIEW_HOLDS should be universal
550 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
553 return $U->storagereq(
554 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
555 $org, $limit, $offset ); # XXX change to workstation
558 __PACKAGE__->register_method (
559 method => 'fetch_hold_notify',
560 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
562 Returns a list of hold notification objects based on hold id.
563 @param authtoken The loggin session key
564 @param holdid The id of the hold whose notifications we want to retrieve
565 @return An array of hold notification objects, event on error.
569 sub fetch_hold_notify {
570 my( $self, $conn, $authtoken, $holdid ) = @_;
571 my( $requestor, $evt ) = $U->checkses($authtoken);
574 ($hold, $evt) = $U->fetch_hold($holdid);
576 ($patron, $evt) = $U->fetch_user($hold->usr);
579 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
582 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
583 return $U->storagereq(
584 'open-ils.storage.direct.action.hold_notification.search.hold.atomic', $holdid );
588 __PACKAGE__->register_method (
589 method => 'create_hold_notify',
590 api_name => 'open-ils.circ.hold_notification.create',
592 Creates a new hold notification object
593 @param authtoken The login session key
594 @param notification The hold notification object to create
595 @return ID of the new object on success, Event on error
598 sub create_hold_notify {
599 my( $self, $conn, $authtoken, $notification ) = @_;
600 my( $requestor, $evt ) = $U->checkses($authtoken);
603 ($hold, $evt) = $U->fetch_hold($notification->hold);
605 ($patron, $evt) = $U->fetch_user($hold->usr);
608 # XXX perm depth probably doesn't matter here -- should always be consortium level
609 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
612 # Set the proper notifier
613 $notification->notify_staff($requestor->id);
614 my $id = $U->storagereq(
615 'open-ils.storage.direct.action.hold_notification.create', $notification );
616 return $U->DB_UPDATE_FAILED($notification) unless $id;
617 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
622 __PACKAGE__->register_method(
623 method => 'reset_hold',
624 api_name => 'open-ils.circ.hold.reset',
626 Un-captures and un-targets a hold, essentially returning
627 it to the state it was in directly after it was placed,
628 then attempts to re-target the hold
629 @param authtoken The login session key
630 @param holdid The id of the hold
636 my( $self, $conn, $auth, $holdid ) = @_;
638 my ($hold, $evt) = $U->fetch_hold($holdid);
640 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
642 $evt = $self->_reset_hold($reqr, $hold);
648 my ($self, $reqr, $hold, $session) = @_;
653 $session = $U->start_db_session();
656 $hold->clear_capture_time;
657 $hold->clear_current_copy;
659 return $U->DB_UPDATE_FAILED($hold) unless
661 'open-ils.storage.direct.action.hold_request.update', $hold )->gather(1);
664 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id )->gather(1);
666 $U->commit_db_session($session) unless $x;