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::CStoreEditor q/:funcs/;
27 use OpenILS::Utils::PermitHold;
28 use OpenSRF::Utils::SettingsClient;
29 use OpenILS::Const qw/:const/;
31 my $apputils = "OpenILS::Application::AppUtils";
36 __PACKAGE__->register_method(
37 method => "create_hold",
38 api_name => "open-ils.circ.holds.create",
40 Create a new hold for an item. From a permissions perspective,
41 the login session is used as the 'requestor' of the hold.
42 The hold recipient is determined by the 'usr' setting within
45 First we verify the requestion has holds request permissions.
46 Then we verify that the recipient is allowed to make the given hold.
47 If not, we see if the requestor has "override" capabilities. If not,
48 a permission exception is returned. If permissions allow, we cycle
49 through the set of holds objects and create.
51 If the recipient does not have permission to place multiple holds
52 on a single title and said operation is attempted, a permission
57 __PACKAGE__->register_method(
58 method => "create_hold",
59 api_name => "open-ils.circ.holds.create.override",
61 If the recipient is not allowed to receive the requested hold,
62 call this method to attempt the override
63 @see open-ils.circ.holds.create
68 my( $self, $conn, $auth, @holds ) = @_;
69 my $e = new_editor(authtoken=>$auth, xact=>1);
70 return $e->event unless $e->checkauth;
72 my $override = 1 if $self->api_name =~ /override/;
74 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
78 for my $hold (@$holds) {
83 my $requestor = $e->requestor;
84 my $recipient = $requestor;
87 if( $requestor->id ne $hold->usr ) {
88 # Make sure the requestor is allowed to place holds for
89 # the recipient if they are not the same people
90 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
91 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
94 # Now make sure the recipient is allowed to receive the specified hold
96 my $porg = $recipient->home_ou;
97 my $rid = $e->requestor->id;
98 my $t = $hold->hold_type;
100 # See if a duplicate hold already exists
102 usr => $recipient->id,
104 fulfillment_time => undef,
105 target => $hold->target,
106 cancel_time => undef,
109 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
111 my $existing = $e->search_action_hold_request($sargs);
112 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
114 if( $t eq OILS_HOLD_TYPE_METARECORD )
115 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'MR_HOLDS'); }
117 if( $t eq OILS_HOLD_TYPE_TITLE )
118 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS'); }
120 if( $t eq OILS_HOLD_TYPE_VOLUME )
121 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
123 if( $t eq OILS_HOLD_TYPE_COPY )
124 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
126 return $pevt if $pevt;
130 for my $evt (@events) {
132 my $name = $evt->{textcode};
133 return $e->event unless $e->allowed("$name.override", $porg);
140 $hold->requestor($e->requestor->id);
141 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
142 $hold = $e->create_action_hold_request($hold) or return $e->event;
143 push( @copyholds, $hold ) if $hold->hold_type eq OILS_HOLD_TYPE_COPY;
148 # Go ahead and target the copy-level holds
150 'open-ils.storage.action.hold_request.copy_targeter',
151 undef, $_->id ) for @copyholds;
157 my( $self, $client, $login_session, @holds) = @_;
159 if(!@holds){return 0;}
160 my( $user, $evt ) = $apputils->checkses($login_session);
164 if(ref($holds[0]) eq 'ARRAY') {
166 } else { $holds = [ @holds ]; }
168 $logger->debug("Iterating over holds requests...");
170 for my $hold (@$holds) {
173 my $type = $hold->hold_type;
175 $logger->activity("User " . $user->id .
176 " creating new hold of type $type for user " . $hold->usr);
179 if($user->id ne $hold->usr) {
180 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
190 # am I allowed to place holds for this user?
191 if($hold->requestor ne $hold->usr) {
192 $perm = _check_request_holds_perm($user->id, $user->home_ou);
193 if($perm) { return $perm; }
196 # is this user allowed to have holds of this type?
197 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
199 #if there is a requestor, see if the requestor has override privelages
200 if($hold->requestor ne $hold->usr) {
201 $perm = _check_request_holds_override($user->id, $user->home_ou);
202 if($perm) {return $perm;}
209 #enforce the fact that the login is the one requesting the hold
210 $hold->requestor($user->id);
211 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
213 my $resp = $apputils->simplereq(
215 'open-ils.storage.direct.action.hold_request.create', $hold );
218 return OpenSRF::EX::ERROR ("Error creating hold");
225 # makes sure that a user has permission to place the type of requested hold
226 # returns the Perm exception if not allowed, returns undef if all is well
227 sub _check_holds_perm {
228 my($type, $user_id, $org_id) = @_;
232 if($evt = $apputils->check_perms(
233 $user_id, $org_id, "MR_HOLDS")) {
237 } elsif ($type eq "T") {
238 if($evt = $apputils->check_perms(
239 $user_id, $org_id, "TITLE_HOLDS")) {
243 } elsif($type eq "V") {
244 if($evt = $apputils->check_perms(
245 $user_id, $org_id, "VOLUME_HOLDS")) {
249 } elsif($type eq "C") {
250 if($evt = $apputils->check_perms(
251 $user_id, $org_id, "COPY_HOLDS")) {
259 # tests if the given user is allowed to place holds on another's behalf
260 sub _check_request_holds_perm {
263 if(my $evt = $apputils->check_perms(
264 $user_id, $org_id, "REQUEST_HOLDS")) {
269 sub _check_request_holds_override {
272 if(my $evt = $apputils->check_perms(
273 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
278 __PACKAGE__->register_method(
279 method => "retrieve_holds_by_id",
280 api_name => "open-ils.circ.holds.retrieve_by_id",
282 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
283 different from the user, then the requestor must have VIEW_HOLD permissions.
287 sub retrieve_holds_by_id {
288 my($self, $client, $auth, $hold_id) = @_;
289 my $e = new_editor(authtoken=>$auth);
290 $e->checkauth or return $e->event;
291 $e->allowed('VIEW_HOLD') or return $e->event;
293 my $holds = $e->search_action_hold_request(
295 { id => $hold_id , fulfillment_time => undef },
296 { order_by => { ahr => "request_time" } }
300 flesh_hold_transits($holds);
305 __PACKAGE__->register_method(
306 method => "retrieve_holds",
307 api_name => "open-ils.circ.holds.retrieve",
309 Retrieves all the holds, with hold transits attached, for the specified
310 user id. The login session is the requestor and if the requestor is
311 different from the user, then the requestor must have VIEW_HOLD permissions.
316 my($self, $client, $login_session, $user_id) = @_;
318 my( $user, $target, $evt ) = $apputils->checkses_requestor(
319 $login_session, $user_id, 'VIEW_HOLD' );
322 my $holds = $apputils->simplereq(
324 "open-ils.cstore.direct.action.hold_request.search.atomic",
327 fulfillment_time => undef,
328 cancel_time => undef,
330 { order_by => { ahr => "request_time" } }
333 for my $hold ( @$holds ) {
335 $apputils->simplereq(
337 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
338 { hold => $hold->id },
339 { order_by => { ahtc => 'id desc' }, limit => 1 }
347 __PACKAGE__->register_method(
348 method => "retrieve_holds_by_pickup_lib",
349 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
351 Retrieves all the holds, with hold transits attached, for the specified
356 sub retrieve_holds_by_pickup_lib {
357 my($self, $client, $login_session, $ou_id) = @_;
359 #FIXME -- put an appropriate permission check here
360 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
361 # $login_session, $user_id, 'VIEW_HOLD' );
362 #return $evt if $evt;
364 my $holds = $apputils->simplereq(
366 "open-ils.cstore.direct.action.hold_request.search.atomic",
368 pickup_lib => $ou_id ,
369 fulfillment_time => undef,
372 { order_by => { ahr => "request_time" } });
375 flesh_hold_transits($holds);
380 __PACKAGE__->register_method(
381 method => "cancel_hold",
382 api_name => "open-ils.circ.hold.cancel",
384 Cancels the specified hold. The login session
385 is the requestor and if the requestor is different from the usr field
386 on the hold, the requestor must have CANCEL_HOLDS permissions.
387 the hold may be either the hold object or the hold id
391 my($self, $client, $auth, $holdid) = @_;
393 my $e = new_editor(authtoken=>$auth, xact=>1);
394 return $e->event unless $e->checkauth;
396 my $hold = $e->retrieve_action_hold_request($holdid)
399 if( $e->requestor->id ne $hold->usr ) {
400 return $e->event unless $e->allowed('CANCEL_HOLDS');
403 return 1 if $hold->cancel_time;
405 # If the hold is captured, reset the copy status
406 if( $hold->capture_time and $hold->current_copy ) {
408 my $copy = $e->retrieve_asset_copy($hold->current_copy)
411 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
412 $logger->info("setting copy to status 'reshelving' on hold cancel");
413 $copy->status(OILS_COPY_STATUS_RESHELVING);
414 $copy->editor($e->requestor->id);
415 $copy->edit_date('now');
416 $e->update_asset_copy($copy) or return $e->event;
420 $hold->cancel_time('now');
421 $e->update_action_hold_request($hold)
424 $self->delete_hold_copy_maps($e, $hold->id);
430 sub delete_hold_copy_maps {
435 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
437 $editor->delete_action_hold_copy_map($_)
438 or return $editor->event;
444 __PACKAGE__->register_method(
445 method => "update_hold",
446 api_name => "open-ils.circ.hold.update",
448 Updates the specified hold. The login session
449 is the requestor and if the requestor is different from the usr field
450 on the hold, the requestor must have UPDATE_HOLDS permissions.
454 my($self, $client, $login_session, $hold) = @_;
456 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
457 $login_session, $hold->usr, 'UPDATE_HOLD' );
460 $logger->activity('User ' . $requestor->id .
461 ' updating hold ' . $hold->id . ' for user ' . $target->id );
463 return $U->storagereq(
464 "open-ils.storage.direct.action.hold_request.update", $hold );
468 __PACKAGE__->register_method(
469 method => "retrieve_hold_status",
470 api_name => "open-ils.circ.hold.status.retrieve",
472 Calculates the current status of the hold.
473 the requestor must have VIEW_HOLD permissions if the hold is for a user
474 other than the requestor.
475 Returns -1 on error (for now)
476 Returns 1 for 'waiting for copy to become available'
477 Returns 2 for 'waiting for copy capture'
478 Returns 3 for 'in transit'
479 Returns 4 for 'arrived'
482 sub retrieve_hold_status {
483 my($self, $client, $auth, $hold_id) = @_;
485 my $e = new_editor(authtoken => $auth);
486 return $e->event unless $e->checkauth;
487 my $hold = $e->retrieve_action_hold_request($hold_id)
490 if( $e->requestor->id != $hold->usr ) {
491 return $e->event unless $e->allowed('VIEW_HOLD');
494 return 1 unless $hold->current_copy;
495 return 2 unless $hold->capture_time;
497 my $copy = $e->retrieve_asset_copy($hold->current_copy)
500 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
501 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
511 __PACKAGE__->register_method(
512 method => "capture_copy",
513 api_name => "open-ils.circ.hold.capture_copy.barcode",
515 Captures a copy to fulfil a hold
516 Params is login session and copy barcode
517 Optional param is 'flesh'. If set, we also return the
518 relevant copy and title
519 login mus have COPY_CHECKIN permissions (since this is essentially
523 # XXX deprecate me XXX
526 my( $self, $client, $login_session, $params ) = @_;
527 my %params = %$params;
528 my $barcode = $params{barcode};
531 my( $user, $target, $copy, $hold, $evt );
533 ( $user, $evt ) = $apputils->checkses($login_session);
536 # am I allowed to checkin a copy?
537 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
540 $logger->info("Capturing copy with barcode $barcode");
542 my $session = $apputils->start_db_session();
544 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
547 $logger->debug("Capturing copy " . $copy->id);
549 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
550 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
553 warn "Found hold " . $hold->id . "\n";
554 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
556 $hold->current_copy($copy->id);
557 $hold->capture_time("now");
560 my $stat = $session->request(
561 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
562 if(!$stat) { throw OpenSRF::EX::ERROR
563 ("Error updating hold request " . $copy->id); }
565 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
567 # if the staff member capturing this item is not at the pickup lib
568 if( $user->home_ou ne $hold->pickup_lib ) {
569 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
572 $copy->editor($user->id);
573 $copy->edit_date("now");
574 $stat = $session->request(
575 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
576 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
578 my $payload = { hold => $hold };
579 $payload->{copy} = $copy if $params{flesh_copy};
581 if($params{flesh_record}) {
583 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
585 $record = $apputils->record_to_mvr($record);
586 $payload->{record} = $record;
589 $apputils->commit_db_session($session);
591 return OpenILS::Event->new('ROUTE_ITEM',
592 route_to => $hold->pickup_lib, payload => $payload );
595 sub _build_hold_transit {
596 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
597 my $trans = Fieldmapper::action::hold_transit_copy->new;
599 $trans->hold($hold->id);
600 $trans->source($user->home_ou);
601 $trans->dest($hold->pickup_lib);
602 $trans->source_send_time("now");
603 $trans->target_copy($copy->id);
604 $trans->copy_status($copy->status);
606 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
607 my ($stat) = $meth->run( $login_session, $trans, $session );
608 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
609 else { $copy->status(6); } #status in transit
614 __PACKAGE__->register_method(
615 method => "create_hold_transit",
616 api_name => "open-ils.circ.hold_transit.create",
618 Creates a new transit object
621 sub create_hold_transit {
622 my( $self, $client, $login_session, $transit, $session ) = @_;
624 my( $user, $evt ) = $apputils->checkses($login_session);
626 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
630 if($session) { $ses = $session; }
631 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
633 return $ses->request(
634 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
640 sub find_local_hold {
641 my( $class, $session, $copy, $user ) = @_;
642 return $class->find_nearest_permitted_hold($session, $copy, $user);
650 sub fetch_open_hold_by_current_copy {
653 my $hold = $apputils->simplereq(
655 'open-ils.cstore.direct.action.hold_request.search.atomic',
656 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
657 return $hold->[0] if ref($hold);
661 sub fetch_related_holds {
664 return $apputils->simplereq(
666 'open-ils.cstore.direct.action.hold_request.search.atomic',
667 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
671 __PACKAGE__->register_method (
672 method => "hold_pull_list",
673 api_name => "open-ils.circ.hold_pull_list.retrieve",
675 Returns a list of hold ID's that need to be "pulled"
681 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
682 my( $reqr, $evt ) = $U->checkses($authtoken);
685 my $org = $reqr->ws_ou || $reqr->home_ou;
686 # the perm locaiton shouldn't really matter here since holds
687 # will exist all over and VIEW_HOLDS should be universal
688 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
691 return $U->storagereq(
692 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
693 $org, $limit, $offset );
696 __PACKAGE__->register_method (
697 method => 'fetch_hold_notify',
698 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
700 Returns a list of hold notification objects based on hold id.
701 @param authtoken The loggin session key
702 @param holdid The id of the hold whose notifications we want to retrieve
703 @return An array of hold notification objects, event on error.
707 sub fetch_hold_notify {
708 my( $self, $conn, $authtoken, $holdid ) = @_;
709 my( $requestor, $evt ) = $U->checkses($authtoken);
712 ($hold, $evt) = $U->fetch_hold($holdid);
714 ($patron, $evt) = $U->fetch_user($hold->usr);
717 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
720 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
721 return $U->cstorereq(
722 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
726 __PACKAGE__->register_method (
727 method => 'create_hold_notify',
728 api_name => 'open-ils.circ.hold_notification.create',
730 Creates a new hold notification object
731 @param authtoken The login session key
732 @param notification The hold notification object to create
733 @return ID of the new object on success, Event on error
736 sub create_hold_notify {
737 my( $self, $conn, $authtoken, $notification ) = @_;
738 my( $requestor, $evt ) = $U->checkses($authtoken);
741 ($hold, $evt) = $U->fetch_hold($notification->hold);
743 ($patron, $evt) = $U->fetch_user($hold->usr);
746 # XXX perm depth probably doesn't matter here -- should always be consortium level
747 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
750 # Set the proper notifier
751 $notification->notify_staff($requestor->id);
752 my $id = $U->storagereq(
753 'open-ils.storage.direct.action.hold_notification.create', $notification );
754 return $U->DB_UPDATE_FAILED($notification) unless $id;
755 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
760 __PACKAGE__->register_method(
761 method => 'reset_hold',
762 api_name => 'open-ils.circ.hold.reset',
764 Un-captures and un-targets a hold, essentially returning
765 it to the state it was in directly after it was placed,
766 then attempts to re-target the hold
767 @param authtoken The login session key
768 @param holdid The id of the hold
774 my( $self, $conn, $auth, $holdid ) = @_;
776 my ($hold, $evt) = $U->fetch_hold($holdid);
778 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
780 $evt = $self->_reset_hold($reqr, $hold);
786 my ($self, $reqr, $hold) = @_;
788 my $e = new_editor(xact =>1, requestor => $reqr);
790 $logger->info("reseting hold ".$hold->id);
792 if( $hold->capture_time and $hold->current_copy ) {
794 my $copy = $e->retrieve_asset_copy($hold->current_copy)
797 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
798 $logger->info("setting copy to status 'reshelving' on hold retarget");
799 $copy->status(OILS_COPY_STATUS_RESHELVING);
800 $copy->editor($e->requestor->id);
801 $copy->edit_date('now');
802 $e->update_asset_copy($copy) or return $e->event;
804 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
805 $logger->warn("reseting hold that is in transit: ".$hold->id);
810 $hold->clear_capture_time;
811 $hold->clear_current_copy;
813 $e->update_action_hold_request($hold) or return $e->event;
818 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
824 __PACKAGE__->register_method(
825 method => 'fetch_open_title_holds',
826 api_name => 'open-ils.circ.open_holds.retrieve',
828 Returns a list ids of un-fulfilled holds for a given title id
829 @param authtoken The login session key
830 @param id the id of the item whose holds we want to retrieve
831 @param type The hold type - M, T, V, C
835 sub fetch_open_title_holds {
836 my( $self, $conn, $auth, $id, $type, $org ) = @_;
837 my $e = new_editor( authtoken => $auth );
838 return $e->event unless $e->checkauth;
841 $org ||= $e->requestor->ws_ou;
843 # return $e->search_action_hold_request(
844 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
846 # XXX make me return IDs in the future ^--
847 my $holds = $e->search_action_hold_request(
850 cancel_time => undef,
852 fulfillment_time => undef
856 flesh_hold_transits($holds);
861 sub flesh_hold_transits {
863 for my $hold ( @$holds ) {
865 $apputils->simplereq(
867 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
868 { hold => $hold->id },
869 { order_by => { ahtc => 'id desc' }, limit => 1 }
875 sub flesh_hold_notices {
876 my( $holds, $e ) = @_;
879 for my $hold (@$holds) {
880 my $notices = $e->search_action_hold_notification(
882 { hold => $hold->id },
883 { order_by => { anh => { 'notify_time desc' } } },
888 $hold->notify_count(scalar(@$notices));
890 my $n = $e->retrieve_action_hold_notification($$notices[0])
892 $hold->notify_time($n->notify_time);
900 __PACKAGE__->register_method(
901 method => 'fetch_captured_holds',
902 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
904 Returns a list ids of un-fulfilled holds for a given title id
905 @param authtoken The login session key
906 @param org The org id of the location in question
909 sub fetch_captured_holds {
910 my( $self, $conn, $auth, $org ) = @_;
912 my $e = new_editor(authtoken => $auth);
913 return $e->event unless $e->checkauth;
914 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
916 $org ||= $e->requestor->ws_ou;
918 my $holds = $e->search_action_hold_request(
920 capture_time => { "!=" => undef },
921 current_copy => { "!=" => undef },
922 fulfillment_time => undef,
924 cancel_time => undef,
929 for my $h (@$holds) {
930 my $copy = $e->retrieve_asset_copy($h->current_copy)
933 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
936 flesh_hold_transits(\@res);
937 flesh_hold_notices(\@res, $e);
945 __PACKAGE__->register_method(
946 method => "check_title_hold",
947 api_name => "open-ils.circ.title_hold.is_possible",
949 Determines if a hold were to be placed by a given user,
950 whether or not said hold would have any potential copies
952 @param authtoken The login session key
953 @param params A hash of named params including:
954 patronid - the id of the hold recipient
955 titleid (brn) - the id of the title to be held
956 depth - the hold range depth (defaults to 0)
959 sub check_title_hold {
960 my( $self, $client, $authtoken, $params ) = @_;
962 my %params = %$params;
963 my $titleid = $params{titleid} ||"";
964 my $mrid = $params{mrid} ||"";
965 my $depth = $params{depth} || 0;
966 my $pickup_lib = $params{pickup_lib};
967 my $hold_type = $params{hold_type} || 'T';
969 my $e = new_editor(authtoken=>$authtoken);
970 return $e->event unless $e->checkauth;
971 my $patron = $e->retrieve_actor_user($params{patronid})
974 if( $e->requestor->id ne $patron->id ) {
975 return $e->event unless
976 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
979 return OpenILS::Event->new('PATRON_BARRED')
980 if $patron->barred and
981 ($patron->barred =~ /t/i or $patron->barred == 1);
983 my $rangelib = $params{range_lib} || $patron->home_ou;
985 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
988 if( $hold_type eq 'T' ) {
989 return _check_title_hold_is_possible(
990 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
993 if( $hold_type eq 'M' ) {
994 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
995 my @recs = map { $_->source } @$maps;
996 for my $rec (@recs) {
997 return 1 if (_check_title_hold_is_possible(
998 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1005 sub _check_title_hold_is_possible {
1006 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1012 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1014 while( $title = $U->storagereq(
1015 'open-ils.storage.biblio.record_entry.ranged_tree',
1016 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1020 ref($title->call_numbers) and
1021 @{$title->call_numbers};
1023 for my $cn (@{$title->call_numbers}) {
1025 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1027 for my $copy (@{$cn->copies}) {
1029 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1031 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1032 { patron => $patron,
1033 requestor => $requestor,
1036 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1037 pickup_lib => $pickup_lib,
1038 request_lib => $request_lib
1042 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1053 sub find_nearest_permitted_hold {
1056 my $session = shift;
1059 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1061 # first see if this copy has already been selected to fulfill a hold
1062 my $hold = $session->request(
1063 "open-ils.storage.direct.action.hold_request.search_where",
1064 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1067 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1071 # We know this hold is permitted, so just return it
1072 return $hold if $hold;
1074 $logger->debug("searching for potential holds at org ".
1075 $user->ws_ou." and copy ".$copy->id);
1077 my $holds = $session->request(
1078 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1079 $user->ws_ou, $copy->id, 5 )->gather(1);
1081 return (undef, $evt) unless @$holds;
1083 # for each potential hold, we have to run the permit script
1084 # to make sure the hold is actually permitted.
1086 for my $holdid (@$holds) {
1087 next unless $holdid;
1088 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1090 my ($hold) = $U->fetch_hold($holdid);
1092 my ($reqr) = $U->fetch_user($hold->requestor);
1094 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1096 patron_id => $hold->usr,
1097 requestor => $reqr->id,
1099 pickup_lib => $hold->pickup_lib,
1100 request_lib => $hold->request_lib
1105 return (undef, $evt);
1109 __PACKAGE__->register_method(
1110 method => 'all_rec_holds',
1111 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1115 my( $self, $conn, $auth, $title_id, $args ) = @_;
1117 my $e = new_editor(authtoken=>$auth);
1118 $e->checkauth or return $e->event;
1119 $e->allowed('VIEW_HOLD') or return $e->event;
1121 $args ||= { fulfillment_time => undef };
1122 $args->{cancel_time} = undef;
1126 $resp->{title_holds} = $e->search_action_hold_request(
1128 hold_type => OILS_HOLD_TYPE_TITLE,
1129 target => $title_id,
1133 my $vols = $e->search_asset_call_number(
1134 { record => $title_id, deleted => 'f' }, {idlist=>1});
1136 $resp->{volume_holds} = (!@$vols) ? [] : $e->search_action_hold_request(
1138 hold_type => OILS_HOLD_TYPE_VOLUME,
1143 my $copies = $e->search_asset_copy(
1144 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1146 $resp->{copy_holds} = (!@$copies) ? [] : $e->search_action_hold_request(
1148 hold_type => OILS_HOLD_TYPE_COPY,