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/OpenILS::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
23 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::CStoreEditor q/:funcs/;
29 use OpenILS::Utils::PermitHold;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenILS::Const qw/:const/;
32 use OpenILS::Application::Circ::Transit;
33 use OpenILS::Application::Actor::Friends;
35 my $apputils = "OpenILS::Application::AppUtils";
41 __PACKAGE__->register_method(
42 method => "create_hold",
43 api_name => "open-ils.circ.holds.create",
45 Create a new hold for an item. From a permissions perspective,
46 the login session is used as the 'requestor' of the hold.
47 The hold recipient is determined by the 'usr' setting within
50 First we verify the requestion has holds request permissions.
51 Then we verify that the recipient is allowed to make the given hold.
52 If not, we see if the requestor has "override" capabilities. If not,
53 a permission exception is returned. If permissions allow, we cycle
54 through the set of holds objects and create.
56 If the recipient does not have permission to place multiple holds
57 on a single title and said operation is attempted, a permission
62 __PACKAGE__->register_method(
63 method => "create_hold",
64 api_name => "open-ils.circ.holds.create.override",
66 If the recipient is not allowed to receive the requested hold,
67 call this method to attempt the override
68 @see open-ils.circ.holds.create
73 my( $self, $conn, $auth, @holds ) = @_;
74 my $e = new_editor(authtoken=>$auth, xact=>1);
75 return $e->event unless $e->checkauth;
77 my $override = 1 if $self->api_name =~ /override/;
79 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
83 for my $hold (@$holds) {
88 my $requestor = $e->requestor;
89 my $recipient = $requestor;
92 if( $requestor->id ne $hold->usr ) {
93 # Make sure the requestor is allowed to place holds for
94 # the recipient if they are not the same people
95 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
96 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
99 # Now make sure the recipient is allowed to receive the specified hold
101 my $porg = $recipient->home_ou;
102 my $rid = $e->requestor->id;
103 my $t = $hold->hold_type;
105 # See if a duplicate hold already exists
107 usr => $recipient->id,
109 fulfillment_time => undef,
110 target => $hold->target,
111 cancel_time => undef,
114 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
116 my $existing = $e->search_action_hold_request($sargs);
117 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
119 if( $t eq OILS_HOLD_TYPE_METARECORD )
120 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
122 if( $t eq OILS_HOLD_TYPE_TITLE )
123 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
125 if( $t eq OILS_HOLD_TYPE_VOLUME )
126 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
128 if( $t eq OILS_HOLD_TYPE_COPY )
129 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
131 return $pevt if $pevt;
135 for my $evt (@events) {
137 my $name = $evt->{textcode};
138 return $e->event unless $e->allowed("$name.override", $porg);
145 # set the configured expire time
146 unless($hold->expire_time) {
147 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
149 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
150 $hold->expire_time($U->epoch2ISO8601($date->epoch));
154 $hold->requestor($e->requestor->id);
155 $hold->request_lib($e->requestor->ws_ou);
156 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
157 $hold = $e->create_action_hold_request($hold) or return $e->event;
162 $conn->respond_complete(1);
165 next if $U->is_true($_->frozen);
167 'open-ils.storage.action.hold_request.copy_targeter',
175 my( $self, $client, $login_session, @holds) = @_;
177 if(!@holds){return 0;}
178 my( $user, $evt ) = $apputils->checkses($login_session);
182 if(ref($holds[0]) eq 'ARRAY') {
184 } else { $holds = [ @holds ]; }
186 $logger->debug("Iterating over holds requests...");
188 for my $hold (@$holds) {
191 my $type = $hold->hold_type;
193 $logger->activity("User " . $user->id .
194 " creating new hold of type $type for user " . $hold->usr);
197 if($user->id ne $hold->usr) {
198 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
208 # am I allowed to place holds for this user?
209 if($hold->requestor ne $hold->usr) {
210 $perm = _check_request_holds_perm($user->id, $user->home_ou);
211 if($perm) { return $perm; }
214 # is this user allowed to have holds of this type?
215 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
216 return $perm if $perm;
218 #enforce the fact that the login is the one requesting the hold
219 $hold->requestor($user->id);
220 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
222 my $resp = $apputils->simplereq(
224 'open-ils.storage.direct.action.hold_request.create', $hold );
227 return OpenSRF::EX::ERROR ("Error creating hold");
234 # makes sure that a user has permission to place the type of requested hold
235 # returns the Perm exception if not allowed, returns undef if all is well
236 sub _check_holds_perm {
237 my($type, $user_id, $org_id) = @_;
241 if($evt = $apputils->check_perms(
242 $user_id, $org_id, "MR_HOLDS")) {
246 } elsif ($type eq "T") {
247 if($evt = $apputils->check_perms(
248 $user_id, $org_id, "TITLE_HOLDS")) {
252 } elsif($type eq "V") {
253 if($evt = $apputils->check_perms(
254 $user_id, $org_id, "VOLUME_HOLDS")) {
258 } elsif($type eq "C") {
259 if($evt = $apputils->check_perms(
260 $user_id, $org_id, "COPY_HOLDS")) {
268 # tests if the given user is allowed to place holds on another's behalf
269 sub _check_request_holds_perm {
272 if(my $evt = $apputils->check_perms(
273 $user_id, $org_id, "REQUEST_HOLDS")) {
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);
301 flesh_hold_notices($holds, $e);
306 __PACKAGE__->register_method(
307 method => "retrieve_holds",
308 api_name => "open-ils.circ.holds.retrieve",
310 Retrieves all the holds, with hold transits attached, for the specified
311 user id. The login session is the requestor and if the requestor is
312 different from the user, then the requestor must have VIEW_HOLD permissions.
315 __PACKAGE__->register_method(
316 method => "retrieve_holds",
318 api_name => "open-ils.circ.holds.id_list.retrieve",
320 Retrieves all the hold ids for the specified
321 user id. The login session is the requestor and if the requestor is
322 different from the user, then the requestor must have VIEW_HOLD permissions.
326 my($self, $client, $auth, $user_id) = @_;
328 my $e = new_editor(authtoken=>$auth);
329 return $e->event unless $e->checkauth;
330 $user_id = $e->requestor->id unless defined $user_id;
332 unless($user_id == $e->requestor->id) {
333 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
334 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
335 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
336 $e, $user_id, $e->requestor->id, 'hold.view');
337 return $e->event unless $allowed;
341 my $holds = $e->search_action_hold_request([
343 fulfillment_time => undef,
344 cancel_time => undef,
346 {order_by => {ahr => "request_time"}}
349 if( ! $self->api_name =~ /id_list/ ) {
350 for my $hold ( @$holds ) {
352 $e->search_action_hold_transit_copy([
354 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
359 if( $self->api_name =~ /id_list/ ) {
360 return [ map { $_->id } @$holds ];
367 __PACKAGE__->register_method(
368 method => 'user_hold_count',
369 api_name => 'open-ils.circ.hold.user.count');
371 sub user_hold_count {
372 my( $self, $conn, $auth, $userid ) = @_;
373 my $e = new_editor(authtoken=>$auth);
374 return $e->event unless $e->checkauth;
375 my $patron = $e->retrieve_actor_user($userid)
377 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
378 return __user_hold_count($self, $e, $userid);
381 sub __user_hold_count {
382 my( $self, $e, $userid ) = @_;
383 my $holds = $e->search_action_hold_request(
385 fulfillment_time => undef,
386 cancel_time => undef,
391 return scalar(@$holds);
395 __PACKAGE__->register_method(
396 method => "retrieve_holds_by_pickup_lib",
397 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
399 Retrieves all the holds, with hold transits attached, for the specified
403 __PACKAGE__->register_method(
404 method => "retrieve_holds_by_pickup_lib",
405 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
407 Retrieves all the hold ids for the specified
411 sub retrieve_holds_by_pickup_lib {
412 my($self, $client, $login_session, $ou_id) = @_;
414 #FIXME -- put an appropriate permission check here
415 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
416 # $login_session, $user_id, 'VIEW_HOLD' );
417 #return $evt if $evt;
419 my $holds = $apputils->simplereq(
421 "open-ils.cstore.direct.action.hold_request.search.atomic",
423 pickup_lib => $ou_id ,
424 fulfillment_time => undef,
427 { order_by => { ahr => "request_time" } });
430 if( ! $self->api_name =~ /id_list/ ) {
431 flesh_hold_transits($holds);
434 if( $self->api_name =~ /id_list/ ) {
435 return [ map { $_->id } @$holds ];
441 __PACKAGE__->register_method(
442 method => "cancel_hold",
443 api_name => "open-ils.circ.hold.cancel",
445 Cancels the specified hold. The login session
446 is the requestor and if the requestor is different from the usr field
447 on the hold, the requestor must have CANCEL_HOLDS permissions.
448 the hold may be either the hold object or the hold id
452 my($self, $client, $auth, $holdid, $cause, $note) = @_;
454 my $e = new_editor(authtoken=>$auth, xact=>1);
455 return $e->event unless $e->checkauth;
457 my $hold = $e->retrieve_action_hold_request($holdid)
460 if( $e->requestor->id ne $hold->usr ) {
461 return $e->event unless $e->allowed('CANCEL_HOLDS');
464 return 1 if $hold->cancel_time;
466 # If the hold is captured, reset the copy status
467 if( $hold->capture_time and $hold->current_copy ) {
469 my $copy = $e->retrieve_asset_copy($hold->current_copy)
472 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
473 $logger->info("canceling hold $holdid whose item is on the holds shelf");
474 # $logger->info("setting copy to status 'reshelving' on hold cancel");
475 # $copy->status(OILS_COPY_STATUS_RESHELVING);
476 # $copy->editor($e->requestor->id);
477 # $copy->edit_date('now');
478 # $e->update_asset_copy($copy) or return $e->event;
480 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
483 $logger->warn("! canceling hold [$hid] that is in transit");
484 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
487 my $trans = $e->retrieve_action_transit_copy($transid);
488 # Leave the transit alive, but set the copy status to
489 # reshelving so it will be properly reshelved when it gets back home
491 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
492 $e->update_action_transit_copy($trans) or return $e->die_event;
498 $hold->cancel_time('now');
499 $hold->cancel_cause($cause);
500 $hold->cancel_note($note);
501 $e->update_action_hold_request($hold)
504 delete_hold_copy_maps($self, $e, $hold->id);
510 sub delete_hold_copy_maps {
515 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
517 $editor->delete_action_hold_copy_map($_)
518 or return $editor->event;
524 __PACKAGE__->register_method(
525 method => "update_hold",
526 api_name => "open-ils.circ.hold.update",
528 Updates the specified hold. The login session
529 is the requestor and if the requestor is different from the usr field
530 on the hold, the requestor must have UPDATE_HOLDS permissions.
534 my($self, $client, $auth, $hold) = @_;
536 my $e = new_editor(authtoken=>$auth, xact=>1);
537 return $e->die_event unless $e->checkauth;
539 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
540 or return $e->die_event;
542 # don't allow the user to be changed
543 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
545 if($hold->usr ne $e->requestor->id) {
546 # if the hold is for a different user, make sure the
547 # requestor has the appropriate permissions
548 my $usr = $e->retrieve_actor_user($hold->usr)
549 or return $e->die_event;
550 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
553 # --------------------------------------------------------------
554 # if the hold is on the holds shelf and the pickup lib changes,
555 # we need to create a new transit
556 # --------------------------------------------------------------
557 if( ($orig_hold->pickup_lib ne $hold->pickup_lib) and (_hold_status($e, $hold) == 4)) {
558 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
559 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
560 my $evt = transit_hold($e, $orig_hold, $hold,
561 $e->retrieve_asset_copy($hold->current_copy));
565 update_hold_if_frozen($self, $e, $hold, $orig_hold);
566 $e->update_action_hold_request($hold) or return $e->die_event;
572 my($e, $orig_hold, $hold, $copy) = @_;
573 my $src = $orig_hold->pickup_lib;
574 my $dest = $hold->pickup_lib;
576 $logger->info("putting hold into transit on pickup_lib update");
578 my $transit = Fieldmapper::action::transit_copy->new;
579 $transit->source($src);
580 $transit->dest($dest);
581 $transit->target_copy($copy->id);
582 $transit->source_send_time('now');
583 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
585 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
586 $copy->editor($e->requestor->id);
587 $copy->edit_date('now');
589 $e->create_action_transit_copy($transit) or return $e->die_event;
590 $e->update_asset_copy($copy) or return $e->die_event;
594 # if the hold is frozen, this method ensures that the hold is not "targeted",
595 # that is, it clears the current_copy and prev_check_time to essentiallly
596 # reset the hold. If it is being activated, it runs the targeter in the background
597 sub update_hold_if_frozen {
598 my($self, $e, $hold, $orig_hold) = @_;
599 return if $hold->capture_time;
601 if($U->is_true($hold->frozen)) {
602 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
603 $hold->clear_current_copy;
604 $hold->clear_prev_check_time;
607 if($U->is_true($orig_hold->frozen)) {
608 $logger->info("Running targeter on activated hold ".$hold->id);
609 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
615 __PACKAGE__->register_method(
616 method => "retrieve_hold_status",
617 api_name => "open-ils.circ.hold.status.retrieve",
619 Calculates the current status of the hold.
620 the requestor must have VIEW_HOLD permissions if the hold is for a user
621 other than the requestor.
622 Returns -1 on error (for now)
623 Returns 1 for 'waiting for copy to become available'
624 Returns 2 for 'waiting for copy capture'
625 Returns 3 for 'in transit'
626 Returns 4 for 'arrived'
629 sub retrieve_hold_status {
630 my($self, $client, $auth, $hold_id) = @_;
632 my $e = new_editor(authtoken => $auth);
633 return $e->event unless $e->checkauth;
634 my $hold = $e->retrieve_action_hold_request($hold_id)
637 if( $e->requestor->id != $hold->usr ) {
638 return $e->event unless $e->allowed('VIEW_HOLD');
641 return _hold_status($e, $hold);
647 return 1 unless $hold->current_copy;
648 return 2 unless $hold->capture_time;
650 my $copy = $hold->current_copy;
651 unless( ref $copy ) {
652 $copy = $e->retrieve_asset_copy($hold->current_copy)
656 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
657 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
664 __PACKAGE__->register_method(
665 method => "retrieve_hold_queue_stats",
666 api_name => "open-ils.circ.hold.queue_stats.retrieve",
669 Returns object with total_holds count, queue_position, potential_copies count, and status code
674 sub retrieve_hold_queue_stats {
675 my($self, $conn, $auth, $hold_id) = @_;
676 my $e = new_editor(authtoken => $auth);
677 return $e->event unless $e->checkauth;
678 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
679 if($e->requestor->id != $hold->usr) {
680 return $e->event unless $e->allowed('VIEW_HOLD');
682 return retrieve_hold_queue_status_impl($e, $hold);
685 sub retrieve_hold_queue_status_impl {
689 my $hold_ids = $e->search_action_hold_request(
691 { target => $hold->target,
692 hold_type => $hold->hold_type,
693 cancel_time => undef,
694 capture_time => undef
696 {order_by => {ahr => 'request_time asc'}}
702 for my $hid (@$hold_ids) {
704 last if $hid == $hold->id;
707 my $potentials = $e->search_action_hold_copy_map({hold => $hold->id}, {idlist => 1});
708 my $num_potentials = scalar(@$potentials);
710 my $user_org = $e->json_query({select => {au => 'home_ou'}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
711 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
712 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
715 total_holds => scalar(@$hold_ids),
716 queue_position => $qpos,
717 potential_copies => $num_potentials,
718 status => _hold_status($e, $hold),
719 estimated_wait => int($estimated_wait)
724 sub fetch_open_hold_by_current_copy {
727 my $hold = $apputils->simplereq(
729 'open-ils.cstore.direct.action.hold_request.search.atomic',
730 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
731 return $hold->[0] if ref($hold);
735 sub fetch_related_holds {
738 return $apputils->simplereq(
740 'open-ils.cstore.direct.action.hold_request.search.atomic',
741 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
745 __PACKAGE__->register_method (
746 method => "hold_pull_list",
747 api_name => "open-ils.circ.hold_pull_list.retrieve",
749 Returns a list of holds that need to be "pulled"
754 __PACKAGE__->register_method (
755 method => "hold_pull_list",
756 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
758 Returns a list of hold ID's that need to be "pulled"
765 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
766 my( $reqr, $evt ) = $U->checkses($authtoken);
769 my $org = $reqr->ws_ou || $reqr->home_ou;
770 # the perm locaiton shouldn't really matter here since holds
771 # will exist all over and VIEW_HOLDS should be universal
772 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
775 if( $self->api_name =~ /id_list/ ) {
776 return $U->storagereq(
777 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
778 $org, $limit, $offset );
780 return $U->storagereq(
781 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
782 $org, $limit, $offset );
786 __PACKAGE__->register_method (
787 method => 'fetch_hold_notify',
788 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
790 Returns a list of hold notification objects based on hold id.
791 @param authtoken The loggin session key
792 @param holdid The id of the hold whose notifications we want to retrieve
793 @return An array of hold notification objects, event on error.
797 sub fetch_hold_notify {
798 my( $self, $conn, $authtoken, $holdid ) = @_;
799 my( $requestor, $evt ) = $U->checkses($authtoken);
802 ($hold, $evt) = $U->fetch_hold($holdid);
804 ($patron, $evt) = $U->fetch_user($hold->usr);
807 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
810 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
811 return $U->cstorereq(
812 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
816 __PACKAGE__->register_method (
817 method => 'create_hold_notify',
818 api_name => 'open-ils.circ.hold_notification.create',
820 Creates a new hold notification object
821 @param authtoken The login session key
822 @param notification The hold notification object to create
823 @return ID of the new object on success, Event on error
827 sub create_hold_notify {
828 my( $self, $conn, $auth, $note ) = @_;
829 my $e = new_editor(authtoken=>$auth, xact=>1);
830 return $e->die_event unless $e->checkauth;
832 my $hold = $e->retrieve_action_hold_request($note->hold)
833 or return $e->die_event;
834 my $patron = $e->retrieve_actor_user($hold->usr)
835 or return $e->die_event;
837 return $e->die_event unless
838 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
840 $note->notify_staff($e->requestor->id);
841 $e->create_action_hold_notification($note) or return $e->die_event;
847 __PACKAGE__->register_method(
848 method => 'reset_hold',
849 api_name => 'open-ils.circ.hold.reset',
851 Un-captures and un-targets a hold, essentially returning
852 it to the state it was in directly after it was placed,
853 then attempts to re-target the hold
854 @param authtoken The login session key
855 @param holdid The id of the hold
861 my( $self, $conn, $auth, $holdid ) = @_;
863 my ($hold, $evt) = $U->fetch_hold($holdid);
865 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
867 $evt = _reset_hold($self, $reqr, $hold);
873 my ($self, $reqr, $hold) = @_;
875 my $e = new_editor(xact =>1, requestor => $reqr);
877 $logger->info("reseting hold ".$hold->id);
881 if( $hold->capture_time and $hold->current_copy ) {
883 my $copy = $e->retrieve_asset_copy($hold->current_copy)
886 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
887 $logger->info("setting copy to status 'reshelving' on hold retarget");
888 $copy->status(OILS_COPY_STATUS_RESHELVING);
889 $copy->editor($e->requestor->id);
890 $copy->edit_date('now');
891 $e->update_asset_copy($copy) or return $e->event;
893 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
895 # We don't want the copy to remain "in transit"
896 $copy->status(OILS_COPY_STATUS_RESHELVING);
897 $logger->warn("! reseting hold [$hid] that is in transit");
898 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
901 my $trans = $e->retrieve_action_transit_copy($transid);
903 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
904 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
905 $logger->info("Transit abort completed with result $evt");
906 return $evt unless "$evt" eq 1;
912 $hold->clear_capture_time;
913 $hold->clear_current_copy;
915 $e->update_action_hold_request($hold) or return $e->event;
919 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
925 __PACKAGE__->register_method(
926 method => 'fetch_open_title_holds',
927 api_name => 'open-ils.circ.open_holds.retrieve',
929 Returns a list ids of un-fulfilled holds for a given title id
930 @param authtoken The login session key
931 @param id the id of the item whose holds we want to retrieve
932 @param type The hold type - M, T, V, C
936 sub fetch_open_title_holds {
937 my( $self, $conn, $auth, $id, $type, $org ) = @_;
938 my $e = new_editor( authtoken => $auth );
939 return $e->event unless $e->checkauth;
942 $org ||= $e->requestor->ws_ou;
944 # return $e->search_action_hold_request(
945 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
947 # XXX make me return IDs in the future ^--
948 my $holds = $e->search_action_hold_request(
951 cancel_time => undef,
953 fulfillment_time => undef
957 flesh_hold_transits($holds);
962 sub flesh_hold_transits {
964 for my $hold ( @$holds ) {
966 $apputils->simplereq(
968 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
969 { hold => $hold->id },
970 { order_by => { ahtc => 'id desc' }, limit => 1 }
976 sub flesh_hold_notices {
977 my( $holds, $e ) = @_;
980 for my $hold (@$holds) {
981 my $notices = $e->search_action_hold_notification(
983 { hold => $hold->id },
984 { order_by => { anh => 'notify_time desc' } },
989 $hold->notify_count(scalar(@$notices));
991 my $n = $e->retrieve_action_hold_notification($$notices[0])
993 $hold->notify_time($n->notify_time);
999 __PACKAGE__->register_method(
1000 method => 'fetch_captured_holds',
1001 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1004 Returns a list of un-fulfilled holds for a given title id
1005 @param authtoken The login session key
1006 @param org The org id of the location in question
1010 __PACKAGE__->register_method(
1011 method => 'fetch_captured_holds',
1012 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1015 Returns a list ids of un-fulfilled holds for a given title id
1016 @param authtoken The login session key
1017 @param org The org id of the location in question
1021 sub fetch_captured_holds {
1022 my( $self, $conn, $auth, $org ) = @_;
1024 my $e = new_editor(authtoken => $auth);
1025 return $e->event unless $e->checkauth;
1026 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1028 $org ||= $e->requestor->ws_ou;
1030 my $hold_ids = $e->json_query(
1032 select => { ahr => ['id'] },
1037 fkey => 'current_copy'
1042 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1044 capture_time => { "!=" => undef },
1045 current_copy => { "!=" => undef },
1046 fulfillment_time => undef,
1048 cancel_time => undef,
1054 for my $hold_id (@$hold_ids) {
1055 if($self->api_name =~ /id_list/) {
1056 $conn->respond($hold_id->{id});
1060 $e->retrieve_action_hold_request([
1064 flesh_fields => {ahr => ['notifications', 'transit']},
1065 order_by => {anh => 'notify_time desc'}
1074 __PACKAGE__->register_method(
1075 method => "check_title_hold",
1076 api_name => "open-ils.circ.title_hold.is_possible",
1078 Determines if a hold were to be placed by a given user,
1079 whether or not said hold would have any potential copies
1081 @param authtoken The login session key
1082 @param params A hash of named params including:
1083 patronid - the id of the hold recipient
1084 titleid (brn) - the id of the title to be held
1085 depth - the hold range depth (defaults to 0)
1088 sub check_title_hold {
1089 my( $self, $client, $authtoken, $params ) = @_;
1091 my %params = %$params;
1092 my $titleid = $params{titleid} ||"";
1093 my $volid = $params{volume_id};
1094 my $copyid = $params{copy_id};
1095 my $mrid = $params{mrid} ||"";
1096 my $depth = $params{depth} || 0;
1097 my $pickup_lib = $params{pickup_lib};
1098 my $hold_type = $params{hold_type} || 'T';
1099 my $selection_ou = $params{selection_ou} || $pickup_lib;
1101 my $e = new_editor(authtoken=>$authtoken);
1102 return $e->event unless $e->checkauth;
1103 my $patron = $e->retrieve_actor_user($params{patronid})
1104 or return $e->event;
1106 if( $e->requestor->id ne $patron->id ) {
1107 return $e->event unless
1108 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1111 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1113 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1114 or return $e->event;
1116 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1117 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1119 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1120 # work up the tree and as soon as we find a potential copy, use that depth
1121 # also, make sure we don't go past the hard boundary if it exists
1123 # our min boundary is the greater of user-specified boundary or hard boundary
1124 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1125 $hard_boundary : $$params{depth};
1127 my $depth = $soft_boundary;
1128 while($depth >= $min_depth) {
1129 $logger->info("performing hold possibility check with soft boundary $depth");
1130 return {success => 1, depth => $depth}
1131 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1134 return {success => 0};
1136 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1137 # there is no soft boundary, enforce the hard boundary if it exists
1138 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1139 if(do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params)) {
1140 return {success => 1, depth => $hard_boundary}
1142 return {success => 0};
1146 # no boundaries defined, fall back to user specifed boundary or no boundary
1147 $logger->info("performing hold possibility check with no boundary");
1148 if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1149 return {success => 1, depth => $hard_boundary};
1151 return {success => 0};
1156 sub do_possibility_checks {
1157 my($e, $patron, $request_lib, $depth, %params) = @_;
1159 my $titleid = $params{titleid} ||"";
1160 my $volid = $params{volume_id};
1161 my $copyid = $params{copy_id};
1162 my $mrid = $params{mrid} ||"";
1163 my $pickup_lib = $params{pickup_lib};
1164 my $hold_type = $params{hold_type} || 'T';
1165 my $selection_ou = $params{selection_ou} || $pickup_lib;
1172 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1174 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1175 $volume = $e->retrieve_asset_call_number($copy->call_number)
1176 or return $e->event;
1177 $title = $e->retrieve_biblio_record_entry($volume->record)
1178 or return $e->event;
1179 return verify_copy_for_hold(
1180 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1182 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1184 $volume = $e->retrieve_asset_call_number($volid)
1185 or return $e->event;
1186 $title = $e->retrieve_biblio_record_entry($volume->record)
1187 or return $e->event;
1189 return _check_volume_hold_is_possible(
1190 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1192 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1194 return _check_title_hold_is_possible(
1195 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1197 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1199 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1200 my @recs = map { $_->source } @$maps;
1201 for my $rec (@recs) {
1202 return 1 if (_check_title_hold_is_possible(
1203 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1211 sub _check_metarecord_hold_is_possible {
1212 my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1214 my $e = new_editor();
1216 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1217 my $copies = $e->json_query(
1219 select => { acp => ['id', 'circ_lib'] },
1224 fkey => 'call_number',
1229 filter => { metarecord => $mrid }
1233 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1234 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1238 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1243 return $e->event unless defined $copies;
1244 $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1245 return 0 unless @$copies;
1247 # -----------------------------------------------------------------------
1248 # sort the copies into buckets based on their circ_lib proximity to
1249 # the patron's home_ou.
1250 # -----------------------------------------------------------------------
1252 my $home_org = $patron->home_ou;
1253 my $req_org = $request_lib->id;
1255 $prox_cache{$home_org} =
1256 $e->search_actor_org_unit_proximity({from_org => $home_org})
1257 unless $prox_cache{$home_org};
1258 my $home_prox = $prox_cache{$home_org};
1261 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1262 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1264 my @keys = sort { $a <=> $b } keys %buckets;
1267 if( $home_org ne $req_org ) {
1268 # -----------------------------------------------------------------------
1269 # shove the copies close to the request_lib into the primary buckets
1270 # directly before the farthest away copies. That way, they are not
1271 # given priority, but they are checked before the farthest copies.
1272 # -----------------------------------------------------------------------
1274 $prox_cache{$req_org} =
1275 $e->search_actor_org_unit_proximity({from_org => $req_org})
1276 unless $prox_cache{$req_org};
1277 my $req_prox = $prox_cache{$req_org};
1280 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1281 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1283 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1284 my $new_key = $highest_key - 0.5; # right before the farthest prox
1285 my @keys2 = sort { $a <=> $b } keys %buckets2;
1286 for my $key (@keys2) {
1287 last if $key >= $highest_key;
1288 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1292 @keys = sort { $a <=> $b } keys %buckets;
1295 for my $key (@keys) {
1296 my @cps = @{$buckets{$key}};
1298 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1300 for my $copyid (@cps) {
1302 next if $seen{$copyid};
1303 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1304 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1305 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1307 my $vol = $e->retrieve_asset_call_number(
1308 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1310 return 1 if verify_copy_for_hold(
1311 $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1319 sub create_ranged_org_filter {
1320 my($e, $selection_ou, $depth) = @_;
1322 # find the orgs from which this hold may be fulfilled,
1323 # based on the selection_ou and depth
1325 my $top_org = $e->search_actor_org_unit([
1326 {parent_ou => undef},
1327 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1330 return () if $depth == $top_org->ou_type->depth;
1332 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1333 %org_filter = (circ_lib => []);
1334 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1336 $logger->info("hold org filter at depth $depth and selection_ou ".
1337 "$selection_ou created list of @{$org_filter{circ_lib}}");
1343 sub _check_title_hold_is_possible {
1344 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1346 my $e = new_editor();
1347 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1349 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1350 my $copies = $e->json_query(
1352 select => { acp => ['id', 'circ_lib'] },
1357 fkey => 'call_number',
1361 filter => { id => $titleid },
1366 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1367 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1371 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1376 return $e->event unless defined $copies;
1377 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1378 return 0 unless @$copies;
1380 # -----------------------------------------------------------------------
1381 # sort the copies into buckets based on their circ_lib proximity to
1382 # the patron's home_ou.
1383 # -----------------------------------------------------------------------
1385 my $home_org = $patron->home_ou;
1386 my $req_org = $request_lib->id;
1388 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1390 $prox_cache{$home_org} =
1391 $e->search_actor_org_unit_proximity({from_org => $home_org})
1392 unless $prox_cache{$home_org};
1393 my $home_prox = $prox_cache{$home_org};
1396 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1397 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1399 my @keys = sort { $a <=> $b } keys %buckets;
1402 if( $home_org ne $req_org ) {
1403 # -----------------------------------------------------------------------
1404 # shove the copies close to the request_lib into the primary buckets
1405 # directly before the farthest away copies. That way, they are not
1406 # given priority, but they are checked before the farthest copies.
1407 # -----------------------------------------------------------------------
1408 $prox_cache{$req_org} =
1409 $e->search_actor_org_unit_proximity({from_org => $req_org})
1410 unless $prox_cache{$req_org};
1411 my $req_prox = $prox_cache{$req_org};
1415 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1416 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1418 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1419 my $new_key = $highest_key - 0.5; # right before the farthest prox
1420 my @keys2 = sort { $a <=> $b } keys %buckets2;
1421 for my $key (@keys2) {
1422 last if $key >= $highest_key;
1423 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1427 @keys = sort { $a <=> $b } keys %buckets;
1431 for my $key (@keys) {
1432 my @cps = @{$buckets{$key}};
1434 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1436 for my $copyid (@cps) {
1438 next if $seen{$copyid};
1439 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1440 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1441 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1443 unless($title) { # grab the title if we don't already have it
1444 my $vol = $e->retrieve_asset_call_number(
1445 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1446 $title = $vol->record;
1449 return 1 if verify_copy_for_hold(
1450 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1459 sub _check_volume_hold_is_possible {
1460 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1461 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1462 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1463 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1464 for my $copy ( @$copies ) {
1465 return 1 if verify_copy_for_hold(
1466 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1473 sub verify_copy_for_hold {
1474 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1475 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1476 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1477 { patron => $patron,
1478 requestor => $requestor,
1481 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1482 pickup_lib => $pickup_lib,
1483 request_lib => $request_lib,
1492 sub find_nearest_permitted_hold {
1495 my $editor = shift; # CStoreEditor object
1496 my $copy = shift; # copy to target
1497 my $user = shift; # staff
1498 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1499 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1501 my $bc = $copy->barcode;
1503 # find any existing holds that already target this copy
1504 my $old_holds = $editor->search_action_hold_request(
1505 { current_copy => $copy->id,
1506 cancel_time => undef,
1507 capture_time => undef
1511 # hold->type "R" means we need this copy
1512 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1515 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1517 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1518 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1520 # search for what should be the best holds for this copy to fulfill
1521 my $best_holds = $U->storagereq(
1522 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1523 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1525 unless(@$best_holds) {
1527 if( my $hold = $$old_holds[0] ) {
1528 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1532 $logger->info("circulator: no suitable holds found for copy $bc");
1533 return (undef, $evt);
1539 # for each potential hold, we have to run the permit script
1540 # to make sure the hold is actually permitted.
1541 for my $holdid (@$best_holds) {
1542 next unless $holdid;
1543 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1545 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1546 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1547 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1549 # see if this hold is permitted
1550 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1551 { patron_id => $hold->usr,
1554 pickup_lib => $hold->pickup_lib,
1555 request_lib => $rlib,
1566 unless( $best_hold ) { # no "good" permitted holds were found
1567 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1568 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1573 $logger->info("circulator: no suitable holds found for copy $bc");
1574 return (undef, $evt);
1577 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1579 # indicate a permitted hold was found
1580 return $best_hold if $check_only;
1582 # we've found a permitted hold. we need to "grab" the copy
1583 # to prevent re-targeted holds (next part) from re-grabbing the copy
1584 $best_hold->current_copy($copy->id);
1585 $editor->update_action_hold_request($best_hold)
1586 or return (undef, $editor->event);
1591 # re-target any other holds that already target this copy
1592 for my $old_hold (@$old_holds) {
1593 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1594 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1595 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1596 $old_hold->clear_current_copy;
1597 $old_hold->clear_prev_check_time;
1598 $editor->update_action_hold_request($old_hold)
1599 or return (undef, $editor->event);
1603 return ($best_hold, undef, $retarget);
1611 __PACKAGE__->register_method(
1612 method => 'all_rec_holds',
1613 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1617 my( $self, $conn, $auth, $title_id, $args ) = @_;
1619 my $e = new_editor(authtoken=>$auth);
1620 $e->checkauth or return $e->event;
1621 $e->allowed('VIEW_HOLD') or return $e->event;
1623 $args ||= { fulfillment_time => undef };
1624 $args->{cancel_time} = undef;
1626 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1628 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1630 $resp->{metarecord_holds} = $e->search_action_hold_request(
1631 { hold_type => OILS_HOLD_TYPE_METARECORD,
1632 target => $mr_map->metarecord,
1638 $resp->{title_holds} = $e->search_action_hold_request(
1640 hold_type => OILS_HOLD_TYPE_TITLE,
1641 target => $title_id,
1645 my $vols = $e->search_asset_call_number(
1646 { record => $title_id, deleted => 'f' }, {idlist=>1});
1648 return $resp unless @$vols;
1650 $resp->{volume_holds} = $e->search_action_hold_request(
1652 hold_type => OILS_HOLD_TYPE_VOLUME,
1657 my $copies = $e->search_asset_copy(
1658 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1660 return $resp unless @$copies;
1662 $resp->{copy_holds} = $e->search_action_hold_request(
1664 hold_type => OILS_HOLD_TYPE_COPY,
1676 __PACKAGE__->register_method(
1677 method => 'uber_hold',
1679 api_name => 'open-ils.circ.hold.details.retrieve'
1683 my($self, $client, $auth, $hold_id) = @_;
1684 my $e = new_editor(authtoken=>$auth);
1685 $e->checkauth or return $e->event;
1686 $e->allowed('VIEW_HOLD') or return $e->event;
1690 my $hold = $e->retrieve_action_hold_request(
1695 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1698 ) or return $e->event;
1700 my $user = $hold->usr;
1701 $hold->usr($user->id);
1703 my $card = $e->retrieve_actor_card($user->card)
1704 or return $e->event;
1706 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1708 flesh_hold_notices([$hold], $e);
1709 flesh_hold_transits([$hold]);
1716 status => _hold_status($e, $hold),
1717 patron_first => $user->first_given_name,
1718 patron_last => $user->family_name,
1719 patron_barcode => $card->barcode,
1725 # -----------------------------------------------------
1726 # Returns the MVR object that represents what the
1728 # -----------------------------------------------------
1730 my( $e, $hold ) = @_;
1736 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1737 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1738 or return $e->event;
1739 $tid = $mr->master_record;
1741 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1742 $tid = $hold->target;
1744 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1745 $volume = $e->retrieve_asset_call_number($hold->target)
1746 or return $e->event;
1747 $tid = $volume->record;
1749 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1750 $copy = $e->retrieve_asset_copy($hold->target)
1751 or return $e->event;
1752 $volume = $e->retrieve_asset_call_number($copy->call_number)
1753 or return $e->event;
1754 $tid = $volume->record;
1757 if(!$copy and ref $hold->current_copy ) {
1758 $copy = $hold->current_copy;
1759 $hold->current_copy($copy->id);
1762 if(!$volume and $copy) {
1763 $volume = $e->retrieve_asset_call_number($copy->call_number);
1766 my $title = $e->retrieve_biblio_record_entry($tid);
1767 return ( $U->record_to_mvr($title), $volume, $copy );