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 use DateTime::Format::ISO8601;
36 my $apputils = "OpenILS::Application::AppUtils";
42 __PACKAGE__->register_method(
43 method => "create_hold",
44 api_name => "open-ils.circ.holds.create",
46 Create a new hold for an item. From a permissions perspective,
47 the login session is used as the 'requestor' of the hold.
48 The hold recipient is determined by the 'usr' setting within
51 First we verify the requestion has holds request permissions.
52 Then we verify that the recipient is allowed to make the given hold.
53 If not, we see if the requestor has "override" capabilities. If not,
54 a permission exception is returned. If permissions allow, we cycle
55 through the set of holds objects and create.
57 If the recipient does not have permission to place multiple holds
58 on a single title and said operation is attempted, a permission
63 __PACKAGE__->register_method(
64 method => "create_hold",
65 api_name => "open-ils.circ.holds.create.override",
67 If the recipient is not allowed to receive the requested hold,
68 call this method to attempt the override
69 @see open-ils.circ.holds.create
74 my( $self, $conn, $auth, @holds ) = @_;
75 my $e = new_editor(authtoken=>$auth, xact=>1);
76 return $e->event unless $e->checkauth;
78 my $override = 1 if $self->api_name =~ /override/;
80 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
84 for my $hold (@$holds) {
89 my $requestor = $e->requestor;
90 my $recipient = $requestor;
93 if( $requestor->id ne $hold->usr ) {
94 # Make sure the requestor is allowed to place holds for
95 # the recipient if they are not the same people
96 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
97 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
100 # Now make sure the recipient is allowed to receive the specified hold
102 my $porg = $recipient->home_ou;
103 my $rid = $e->requestor->id;
104 my $t = $hold->hold_type;
106 # See if a duplicate hold already exists
108 usr => $recipient->id,
110 fulfillment_time => undef,
111 target => $hold->target,
112 cancel_time => undef,
115 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
117 my $existing = $e->search_action_hold_request($sargs);
118 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
120 if( $t eq OILS_HOLD_TYPE_METARECORD )
121 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
123 if( $t eq OILS_HOLD_TYPE_TITLE )
124 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
126 if( $t eq OILS_HOLD_TYPE_VOLUME )
127 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
129 if( $t eq OILS_HOLD_TYPE_COPY )
130 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
132 return $pevt if $pevt;
136 for my $evt (@events) {
138 my $name = $evt->{textcode};
139 return $e->event unless $e->allowed("$name.override", $porg);
146 # set the configured expire time
147 unless($hold->expire_time) {
148 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
150 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
151 $hold->expire_time($U->epoch2ISO8601($date->epoch));
155 $hold->requestor($e->requestor->id);
156 $hold->request_lib($e->requestor->ws_ou);
157 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
158 $hold = $e->create_action_hold_request($hold) or return $e->event;
163 $conn->respond_complete(1);
166 next if $U->is_true($_->frozen);
168 'open-ils.storage.action.hold_request.copy_targeter',
176 my( $self, $client, $login_session, @holds) = @_;
178 if(!@holds){return 0;}
179 my( $user, $evt ) = $apputils->checkses($login_session);
183 if(ref($holds[0]) eq 'ARRAY') {
185 } else { $holds = [ @holds ]; }
187 $logger->debug("Iterating over holds requests...");
189 for my $hold (@$holds) {
192 my $type = $hold->hold_type;
194 $logger->activity("User " . $user->id .
195 " creating new hold of type $type for user " . $hold->usr);
198 if($user->id ne $hold->usr) {
199 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
209 # am I allowed to place holds for this user?
210 if($hold->requestor ne $hold->usr) {
211 $perm = _check_request_holds_perm($user->id, $user->home_ou);
212 if($perm) { return $perm; }
215 # is this user allowed to have holds of this type?
216 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
217 return $perm if $perm;
219 #enforce the fact that the login is the one requesting the hold
220 $hold->requestor($user->id);
221 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
223 my $resp = $apputils->simplereq(
225 'open-ils.storage.direct.action.hold_request.create', $hold );
228 return OpenSRF::EX::ERROR ("Error creating hold");
235 # makes sure that a user has permission to place the type of requested hold
236 # returns the Perm exception if not allowed, returns undef if all is well
237 sub _check_holds_perm {
238 my($type, $user_id, $org_id) = @_;
242 if($evt = $apputils->check_perms(
243 $user_id, $org_id, "MR_HOLDS")) {
247 } elsif ($type eq "T") {
248 if($evt = $apputils->check_perms(
249 $user_id, $org_id, "TITLE_HOLDS")) {
253 } elsif($type eq "V") {
254 if($evt = $apputils->check_perms(
255 $user_id, $org_id, "VOLUME_HOLDS")) {
259 } elsif($type eq "C") {
260 if($evt = $apputils->check_perms(
261 $user_id, $org_id, "COPY_HOLDS")) {
269 # tests if the given user is allowed to place holds on another's behalf
270 sub _check_request_holds_perm {
273 if(my $evt = $apputils->check_perms(
274 $user_id, $org_id, "REQUEST_HOLDS")) {
279 __PACKAGE__->register_method(
280 method => "retrieve_holds_by_id",
281 api_name => "open-ils.circ.holds.retrieve_by_id",
283 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
284 different from the user, then the requestor must have VIEW_HOLD permissions.
288 sub retrieve_holds_by_id {
289 my($self, $client, $auth, $hold_id) = @_;
290 my $e = new_editor(authtoken=>$auth);
291 $e->checkauth or return $e->event;
292 $e->allowed('VIEW_HOLD') or return $e->event;
294 my $holds = $e->search_action_hold_request(
296 { id => $hold_id , fulfillment_time => undef },
297 { order_by => { ahr => "request_time" } }
301 flesh_hold_transits($holds);
302 flesh_hold_notices($holds, $e);
307 __PACKAGE__->register_method(
308 method => "retrieve_holds",
309 api_name => "open-ils.circ.holds.retrieve",
311 Retrieves all the holds, with hold transits attached, for the specified
312 user id. The login session is the requestor and if the requestor is
313 different from the user, then the requestor must have VIEW_HOLD permissions.
316 __PACKAGE__->register_method(
317 method => "retrieve_holds",
319 api_name => "open-ils.circ.holds.id_list.retrieve",
321 Retrieves all the hold ids for the specified
322 user id. The login session is the requestor and if the requestor is
323 different from the user, then the requestor must have VIEW_HOLD permissions.
327 my($self, $client, $auth, $user_id, $options) = @_;
329 my $e = new_editor(authtoken=>$auth);
330 return $e->event unless $e->checkauth;
331 $user_id = $e->requestor->id unless defined $user_id;
334 unless($user_id == $e->requestor->id) {
335 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
336 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
337 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
338 $e, $user_id, $e->requestor->id, 'hold.view');
339 return $e->event unless $allowed;
343 my $holds = $e->search_action_hold_request([
345 fulfillment_time => undef,
346 cancel_time => undef,
348 {order_by => {ahr => "request_time"}}
351 if($$options{canceled}) {
352 my $count = $$options{cancel_count} ||
353 $U->ou_ancestor_setting_value($e->requestor->ws_ou,
354 'circ.canceled_hold_display_count', $e) || 5;
356 my $canceled = $e->search_action_hold_request([
358 fulfillment_time => undef,
359 cancel_time => {'!=' => undef},
361 {order_by => {ahr => "cancel_time desc"}, limit => $count}
363 push(@$holds, @$canceled);
366 if( ! $self->api_name =~ /id_list/ ) {
367 for my $hold ( @$holds ) {
369 $e->search_action_hold_transit_copy([
371 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
376 if( $self->api_name =~ /id_list/ ) {
377 return [ map { $_->id } @$holds ];
384 __PACKAGE__->register_method(
385 method => 'user_hold_count',
386 api_name => 'open-ils.circ.hold.user.count');
388 sub user_hold_count {
389 my( $self, $conn, $auth, $userid ) = @_;
390 my $e = new_editor(authtoken=>$auth);
391 return $e->event unless $e->checkauth;
392 my $patron = $e->retrieve_actor_user($userid)
394 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
395 return __user_hold_count($self, $e, $userid);
398 sub __user_hold_count {
399 my( $self, $e, $userid ) = @_;
400 my $holds = $e->search_action_hold_request(
402 fulfillment_time => undef,
403 cancel_time => undef,
408 return scalar(@$holds);
412 __PACKAGE__->register_method(
413 method => "retrieve_holds_by_pickup_lib",
414 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
416 Retrieves all the holds, with hold transits attached, for the specified
420 __PACKAGE__->register_method(
421 method => "retrieve_holds_by_pickup_lib",
422 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
424 Retrieves all the hold ids for the specified
428 sub retrieve_holds_by_pickup_lib {
429 my($self, $client, $login_session, $ou_id) = @_;
431 #FIXME -- put an appropriate permission check here
432 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
433 # $login_session, $user_id, 'VIEW_HOLD' );
434 #return $evt if $evt;
436 my $holds = $apputils->simplereq(
438 "open-ils.cstore.direct.action.hold_request.search.atomic",
440 pickup_lib => $ou_id ,
441 fulfillment_time => undef,
444 { order_by => { ahr => "request_time" } });
447 if( ! $self->api_name =~ /id_list/ ) {
448 flesh_hold_transits($holds);
451 if( $self->api_name =~ /id_list/ ) {
452 return [ map { $_->id } @$holds ];
459 __PACKAGE__->register_method(
460 method => "uncancel_hold",
461 api_name => "open-ils.circ.hold.uncancel"
465 my($self, $client, $auth, $hold_id) = @_;
466 my $e = new_editor(authtoken=>$auth, xact=>1);
467 return $e->event unless $e->checkauth;
469 my $hold = $e->retrieve_action_hold_request($hold_id)
470 or return $e->die_event;
471 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
473 return 0 if $hold->fulfillment_time;
474 return 1 unless $hold->cancel_time;
476 # if configured to reset the request time, also reset the expire time
477 if($U->ou_ancestor_setting_value(
478 $hold->request_lib, 'circ.hold_reset_request_time_on_uncancel', $e)) {
480 $hold->request_time('now');
481 my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
483 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
484 $hold->expire_time($U->epoch2ISO8601($date->epoch));
488 $hold->clear_cancel_time;
489 $e->update_action_hold_request($hold) or return $e->die_event;
492 $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
498 __PACKAGE__->register_method(
499 method => "cancel_hold",
500 api_name => "open-ils.circ.hold.cancel",
502 Cancels the specified hold. The login session
503 is the requestor and if the requestor is different from the usr field
504 on the hold, the requestor must have CANCEL_HOLDS permissions.
505 the hold may be either the hold object or the hold id
509 my($self, $client, $auth, $holdid, $cause, $note) = @_;
511 my $e = new_editor(authtoken=>$auth, xact=>1);
512 return $e->event unless $e->checkauth;
514 my $hold = $e->retrieve_action_hold_request($holdid)
517 if( $e->requestor->id ne $hold->usr ) {
518 return $e->event unless $e->allowed('CANCEL_HOLDS');
521 return 1 if $hold->cancel_time;
523 # If the hold is captured, reset the copy status
524 if( $hold->capture_time and $hold->current_copy ) {
526 my $copy = $e->retrieve_asset_copy($hold->current_copy)
529 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
530 $logger->info("canceling hold $holdid whose item is on the holds shelf");
531 # $logger->info("setting copy to status 'reshelving' on hold cancel");
532 # $copy->status(OILS_COPY_STATUS_RESHELVING);
533 # $copy->editor($e->requestor->id);
534 # $copy->edit_date('now');
535 # $e->update_asset_copy($copy) or return $e->event;
537 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
540 $logger->warn("! canceling hold [$hid] that is in transit");
541 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
544 my $trans = $e->retrieve_action_transit_copy($transid);
545 # Leave the transit alive, but set the copy status to
546 # reshelving so it will be properly reshelved when it gets back home
548 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
549 $e->update_action_transit_copy($trans) or return $e->die_event;
555 $hold->cancel_time('now');
556 $hold->cancel_cause($cause);
557 $hold->cancel_note($note);
558 $e->update_action_hold_request($hold)
561 delete_hold_copy_maps($self, $e, $hold->id);
567 sub delete_hold_copy_maps {
572 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
574 $editor->delete_action_hold_copy_map($_)
575 or return $editor->event;
581 __PACKAGE__->register_method(
582 method => "update_hold",
583 api_name => "open-ils.circ.hold.update",
585 Updates the specified hold. The login session
586 is the requestor and if the requestor is different from the usr field
587 on the hold, the requestor must have UPDATE_HOLDS permissions.
591 my($self, $client, $auth, $hold) = @_;
593 my $e = new_editor(authtoken=>$auth, xact=>1);
594 return $e->die_event unless $e->checkauth;
596 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
597 or return $e->die_event;
599 # don't allow the user to be changed
600 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
602 if($hold->usr ne $e->requestor->id) {
603 # if the hold is for a different user, make sure the
604 # requestor has the appropriate permissions
605 my $usr = $e->retrieve_actor_user($hold->usr)
606 or return $e->die_event;
607 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
610 # --------------------------------------------------------------
611 # if the hold is on the holds shelf and the pickup lib changes,
612 # we need to create a new transit
613 # --------------------------------------------------------------
614 if( ($orig_hold->pickup_lib ne $hold->pickup_lib) and (_hold_status($e, $hold) == 4)) {
615 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
616 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
617 my $evt = transit_hold($e, $orig_hold, $hold,
618 $e->retrieve_asset_copy($hold->current_copy));
622 update_hold_if_frozen($self, $e, $hold, $orig_hold);
623 $e->update_action_hold_request($hold) or return $e->die_event;
629 my($e, $orig_hold, $hold, $copy) = @_;
630 my $src = $orig_hold->pickup_lib;
631 my $dest = $hold->pickup_lib;
633 $logger->info("putting hold into transit on pickup_lib update");
635 my $transit = Fieldmapper::action::transit_copy->new;
636 $transit->source($src);
637 $transit->dest($dest);
638 $transit->target_copy($copy->id);
639 $transit->source_send_time('now');
640 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
642 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
643 $copy->editor($e->requestor->id);
644 $copy->edit_date('now');
646 $e->create_action_transit_copy($transit) or return $e->die_event;
647 $e->update_asset_copy($copy) or return $e->die_event;
651 # if the hold is frozen, this method ensures that the hold is not "targeted",
652 # that is, it clears the current_copy and prev_check_time to essentiallly
653 # reset the hold. If it is being activated, it runs the targeter in the background
654 sub update_hold_if_frozen {
655 my($self, $e, $hold, $orig_hold) = @_;
656 return if $hold->capture_time;
658 if($U->is_true($hold->frozen)) {
659 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
660 $hold->clear_current_copy;
661 $hold->clear_prev_check_time;
664 if($U->is_true($orig_hold->frozen)) {
665 $logger->info("Running targeter on activated hold ".$hold->id);
666 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
670 __PACKAGE__->register_method(
671 method => "hold_note_CUD",
672 api_name => "open-ils.circ.hold_request.note.cud");
675 my($self, $conn, $auth, $note) = @_;
677 my $e = new_editor(authtoken => $auth, xact => 1);
678 return $e->die_event unless $e->checkauth;
680 my $hold = $e->retrieve_action_hold_request($note->hold)
681 or return $e->die_event;
683 if($hold->usr ne $e->requestor->id) {
684 my $usr = $e->retrieve_actor_user($hold->usr);
685 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
686 $note->staff('t') if $note->isnew;
690 $e->create_action_hold_request_note($note) or return $e->die_event;
691 } elsif($note->ischanged) {
692 $e->update_action_hold_request_note($note) or return $e->die_event;
693 } elsif($note->isdeleted) {
694 $e->delete_action_hold_request_note($note) or return $e->die_event;
703 __PACKAGE__->register_method(
704 method => "retrieve_hold_status",
705 api_name => "open-ils.circ.hold.status.retrieve",
707 Calculates the current status of the hold.
708 the requestor must have VIEW_HOLD permissions if the hold is for a user
709 other than the requestor.
710 Returns -1 on error (for now)
711 Returns 1 for 'waiting for copy to become available'
712 Returns 2 for 'waiting for copy capture'
713 Returns 3 for 'in transit'
714 Returns 4 for 'arrived'
715 Returns 5 for 'hold-shelf-delay'
718 sub retrieve_hold_status {
719 my($self, $client, $auth, $hold_id) = @_;
721 my $e = new_editor(authtoken => $auth);
722 return $e->event unless $e->checkauth;
723 my $hold = $e->retrieve_action_hold_request($hold_id)
726 if( $e->requestor->id != $hold->usr ) {
727 return $e->event unless $e->allowed('VIEW_HOLD');
730 return _hold_status($e, $hold);
736 return 1 unless $hold->current_copy;
737 return 2 unless $hold->capture_time;
739 my $copy = $hold->current_copy;
740 unless( ref $copy ) {
741 $copy = $e->retrieve_asset_copy($hold->current_copy)
745 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
747 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
749 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
750 return 4 unless $hs_wait_interval;
752 # if a hold_shelf_status_delay interval is defined and start_time plus
753 # the interval is greater than now, consider the hold to be in the virtual
754 # "on its way to the holds shelf" status. Return 5.
756 my $transit = $e->search_action_hold_transit({hold => $hold->id})->[0];
757 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
758 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clense_ISO8601($start_time));
759 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
761 return 5 if $end_time > DateTime->now;
770 __PACKAGE__->register_method(
771 method => "retrieve_hold_queue_stats",
772 api_name => "open-ils.circ.hold.queue_stats.retrieve",
775 Returns object with total_holds count, queue_position, potential_copies count, and status code
780 sub retrieve_hold_queue_stats {
781 my($self, $conn, $auth, $hold_id) = @_;
782 my $e = new_editor(authtoken => $auth);
783 return $e->event unless $e->checkauth;
784 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
785 if($e->requestor->id != $hold->usr) {
786 return $e->event unless $e->allowed('VIEW_HOLD');
788 return retrieve_hold_queue_status_impl($e, $hold);
791 sub retrieve_hold_queue_status_impl {
795 my $hold_ids = $e->search_action_hold_request(
797 { target => $hold->target,
798 hold_type => $hold->hold_type,
799 cancel_time => undef,
800 fulfillment_time => undef
802 {order_by => {ahr => 'request_time asc'}}
808 for my $hid (@$hold_ids) {
809 last if $hid == $hold->id;
813 my $potentials = $e->search_action_hold_copy_map({hold => $hold->id}, {idlist => 1});
814 my $num_potentials = scalar(@$potentials);
816 my $user_org = $e->json_query({select => {au => 'home_ou'}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
817 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
818 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
821 total_holds => scalar(@$hold_ids),
822 queue_position => $qpos,
823 potential_copies => $num_potentials,
824 status => _hold_status($e, $hold),
825 estimated_wait => int($estimated_wait)
830 sub fetch_open_hold_by_current_copy {
833 my $hold = $apputils->simplereq(
835 'open-ils.cstore.direct.action.hold_request.search.atomic',
836 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
837 return $hold->[0] if ref($hold);
841 sub fetch_related_holds {
844 return $apputils->simplereq(
846 'open-ils.cstore.direct.action.hold_request.search.atomic',
847 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
851 __PACKAGE__->register_method (
852 method => "hold_pull_list",
853 api_name => "open-ils.circ.hold_pull_list.retrieve",
855 Returns a list of holds that need to be "pulled"
860 __PACKAGE__->register_method (
861 method => "hold_pull_list",
862 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
864 Returns a list of hold ID's that need to be "pulled"
871 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
872 my( $reqr, $evt ) = $U->checkses($authtoken);
875 my $org = $reqr->ws_ou || $reqr->home_ou;
876 # the perm locaiton shouldn't really matter here since holds
877 # will exist all over and VIEW_HOLDS should be universal
878 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
881 if( $self->api_name =~ /id_list/ ) {
882 return $U->storagereq(
883 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
884 $org, $limit, $offset );
886 return $U->storagereq(
887 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
888 $org, $limit, $offset );
892 __PACKAGE__->register_method (
893 method => 'fetch_hold_notify',
894 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
896 Returns a list of hold notification objects based on hold id.
897 @param authtoken The loggin session key
898 @param holdid The id of the hold whose notifications we want to retrieve
899 @return An array of hold notification objects, event on error.
903 sub fetch_hold_notify {
904 my( $self, $conn, $authtoken, $holdid ) = @_;
905 my( $requestor, $evt ) = $U->checkses($authtoken);
908 ($hold, $evt) = $U->fetch_hold($holdid);
910 ($patron, $evt) = $U->fetch_user($hold->usr);
913 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
916 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
917 return $U->cstorereq(
918 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
922 __PACKAGE__->register_method (
923 method => 'create_hold_notify',
924 api_name => 'open-ils.circ.hold_notification.create',
926 Creates a new hold notification object
927 @param authtoken The login session key
928 @param notification The hold notification object to create
929 @return ID of the new object on success, Event on error
933 sub create_hold_notify {
934 my( $self, $conn, $auth, $note ) = @_;
935 my $e = new_editor(authtoken=>$auth, xact=>1);
936 return $e->die_event unless $e->checkauth;
938 my $hold = $e->retrieve_action_hold_request($note->hold)
939 or return $e->die_event;
940 my $patron = $e->retrieve_actor_user($hold->usr)
941 or return $e->die_event;
943 return $e->die_event unless
944 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
946 $note->notify_staff($e->requestor->id);
947 $e->create_action_hold_notification($note) or return $e->die_event;
953 __PACKAGE__->register_method(
954 method => 'reset_hold',
955 api_name => 'open-ils.circ.hold.reset',
957 Un-captures and un-targets a hold, essentially returning
958 it to the state it was in directly after it was placed,
959 then attempts to re-target the hold
960 @param authtoken The login session key
961 @param holdid The id of the hold
967 my( $self, $conn, $auth, $holdid ) = @_;
969 my ($hold, $evt) = $U->fetch_hold($holdid);
971 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
973 $evt = _reset_hold($self, $reqr, $hold);
979 __PACKAGE__->register_method(
980 method => 'reset_hold_batch',
981 api_name => 'open-ils.circ.hold.reset.batch'
984 sub reset_hold_batch {
985 my($self, $conn, $auth, $hold_ids) = @_;
987 my $e = new_editor(authtoken => $auth);
988 return $e->event unless $e->checkauth;
990 for my $hold_id ($hold_ids) {
992 my $hold = $e->retrieve_action_hold_request(
993 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
996 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
997 _reset_hold($self, $e->requestor, $hold);
1005 my ($self, $reqr, $hold) = @_;
1007 my $e = new_editor(xact =>1, requestor => $reqr);
1009 $logger->info("reseting hold ".$hold->id);
1011 my $hid = $hold->id;
1013 if( $hold->capture_time and $hold->current_copy ) {
1015 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1016 or return $e->event;
1018 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1019 $logger->info("setting copy to status 'reshelving' on hold retarget");
1020 $copy->status(OILS_COPY_STATUS_RESHELVING);
1021 $copy->editor($e->requestor->id);
1022 $copy->edit_date('now');
1023 $e->update_asset_copy($copy) or return $e->event;
1025 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1027 # We don't want the copy to remain "in transit"
1028 $copy->status(OILS_COPY_STATUS_RESHELVING);
1029 $logger->warn("! reseting hold [$hid] that is in transit");
1030 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1033 my $trans = $e->retrieve_action_transit_copy($transid);
1035 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1036 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1037 $logger->info("Transit abort completed with result $evt");
1038 return $evt unless "$evt" eq 1;
1044 $hold->clear_capture_time;
1045 $hold->clear_current_copy;
1047 $e->update_action_hold_request($hold) or return $e->event;
1051 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1057 __PACKAGE__->register_method(
1058 method => 'fetch_open_title_holds',
1059 api_name => 'open-ils.circ.open_holds.retrieve',
1061 Returns a list ids of un-fulfilled holds for a given title id
1062 @param authtoken The login session key
1063 @param id the id of the item whose holds we want to retrieve
1064 @param type The hold type - M, T, V, C
1068 sub fetch_open_title_holds {
1069 my( $self, $conn, $auth, $id, $type, $org ) = @_;
1070 my $e = new_editor( authtoken => $auth );
1071 return $e->event unless $e->checkauth;
1074 $org ||= $e->requestor->ws_ou;
1076 # return $e->search_action_hold_request(
1077 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1079 # XXX make me return IDs in the future ^--
1080 my $holds = $e->search_action_hold_request(
1083 cancel_time => undef,
1085 fulfillment_time => undef
1089 flesh_hold_transits($holds);
1094 sub flesh_hold_transits {
1096 for my $hold ( @$holds ) {
1098 $apputils->simplereq(
1100 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1101 { hold => $hold->id },
1102 { order_by => { ahtc => 'id desc' }, limit => 1 }
1108 sub flesh_hold_notices {
1109 my( $holds, $e ) = @_;
1110 $e ||= new_editor();
1112 for my $hold (@$holds) {
1113 my $notices = $e->search_action_hold_notification(
1115 { hold => $hold->id },
1116 { order_by => { anh => 'notify_time desc' } },
1121 $hold->notify_count(scalar(@$notices));
1123 my $n = $e->retrieve_action_hold_notification($$notices[0])
1124 or return $e->event;
1125 $hold->notify_time($n->notify_time);
1131 __PACKAGE__->register_method(
1132 method => 'fetch_captured_holds',
1133 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1136 Returns a list of un-fulfilled holds for a given title id
1137 @param authtoken The login session key
1138 @param org The org id of the location in question
1142 __PACKAGE__->register_method(
1143 method => 'fetch_captured_holds',
1144 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1147 Returns a list ids of un-fulfilled holds for a given title id
1148 @param authtoken The login session key
1149 @param org The org id of the location in question
1153 sub fetch_captured_holds {
1154 my( $self, $conn, $auth, $org ) = @_;
1156 my $e = new_editor(authtoken => $auth);
1157 return $e->event unless $e->checkauth;
1158 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1160 $org ||= $e->requestor->ws_ou;
1162 my $hold_ids = $e->json_query(
1164 select => { ahr => ['id'] },
1169 fkey => 'current_copy'
1174 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1176 capture_time => { "!=" => undef },
1177 current_copy => { "!=" => undef },
1178 fulfillment_time => undef,
1180 cancel_time => undef,
1186 for my $hold_id (@$hold_ids) {
1187 if($self->api_name =~ /id_list/) {
1188 $conn->respond($hold_id->{id});
1192 $e->retrieve_action_hold_request([
1196 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1197 order_by => {anh => 'notify_time desc'}
1206 __PACKAGE__->register_method(
1207 method => "check_title_hold",
1208 api_name => "open-ils.circ.title_hold.is_possible",
1210 Determines if a hold were to be placed by a given user,
1211 whether or not said hold would have any potential copies
1213 @param authtoken The login session key
1214 @param params A hash of named params including:
1215 patronid - the id of the hold recipient
1216 titleid (brn) - the id of the title to be held
1217 depth - the hold range depth (defaults to 0)
1220 sub check_title_hold {
1221 my( $self, $client, $authtoken, $params ) = @_;
1223 my %params = %$params;
1224 my $titleid = $params{titleid} ||"";
1225 my $volid = $params{volume_id};
1226 my $copyid = $params{copy_id};
1227 my $mrid = $params{mrid} ||"";
1228 my $depth = $params{depth} || 0;
1229 my $pickup_lib = $params{pickup_lib};
1230 my $hold_type = $params{hold_type} || 'T';
1231 my $selection_ou = $params{selection_ou} || $pickup_lib;
1233 my $e = new_editor(authtoken=>$authtoken);
1234 return $e->event unless $e->checkauth;
1235 my $patron = $e->retrieve_actor_user($params{patronid})
1236 or return $e->event;
1238 if( $e->requestor->id ne $patron->id ) {
1239 return $e->event unless
1240 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1243 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1245 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1246 or return $e->event;
1248 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1249 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1251 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1252 # work up the tree and as soon as we find a potential copy, use that depth
1253 # also, make sure we don't go past the hard boundary if it exists
1255 # our min boundary is the greater of user-specified boundary or hard boundary
1256 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1257 $hard_boundary : $$params{depth};
1259 my $depth = $soft_boundary;
1260 while($depth >= $min_depth) {
1261 $logger->info("performing hold possibility check with soft boundary $depth");
1262 my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1263 return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1266 return {success => 0};
1268 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1269 # there is no soft boundary, enforce the hard boundary if it exists
1270 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1271 my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1273 return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1275 return {success => 0};
1279 # no boundaries defined, fall back to user specifed boundary or no boundary
1280 $logger->info("performing hold possibility check with no boundary");
1281 my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1283 return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1285 return {success => 0};
1290 sub do_possibility_checks {
1291 my($e, $patron, $request_lib, $depth, %params) = @_;
1293 my $titleid = $params{titleid} ||"";
1294 my $volid = $params{volume_id};
1295 my $copyid = $params{copy_id};
1296 my $mrid = $params{mrid} ||"";
1297 my $pickup_lib = $params{pickup_lib};
1298 my $hold_type = $params{hold_type} || 'T';
1299 my $selection_ou = $params{selection_ou} || $pickup_lib;
1306 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1308 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1309 $volume = $e->retrieve_asset_call_number($copy->call_number)
1310 or return $e->event;
1311 $title = $e->retrieve_biblio_record_entry($volume->record)
1312 or return $e->event;
1313 return verify_copy_for_hold(
1314 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1316 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1318 $volume = $e->retrieve_asset_call_number($volid)
1319 or return $e->event;
1320 $title = $e->retrieve_biblio_record_entry($volume->record)
1321 or return $e->event;
1323 return _check_volume_hold_is_possible(
1324 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1326 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1328 return _check_title_hold_is_possible(
1329 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1331 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1333 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1334 my @recs = map { $_->source } @$maps;
1335 for my $rec (@recs) {
1336 my @status = _check_title_hold_is_possible(
1337 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1338 return @status if $status[1];
1345 sub create_ranged_org_filter {
1346 my($e, $selection_ou, $depth) = @_;
1348 # find the orgs from which this hold may be fulfilled,
1349 # based on the selection_ou and depth
1351 my $top_org = $e->search_actor_org_unit([
1352 {parent_ou => undef},
1353 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1356 return () if $depth == $top_org->ou_type->depth;
1358 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1359 %org_filter = (circ_lib => []);
1360 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1362 $logger->info("hold org filter at depth $depth and selection_ou ".
1363 "$selection_ou created list of @{$org_filter{circ_lib}}");
1369 sub _check_title_hold_is_possible {
1370 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1372 my $e = new_editor();
1373 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1375 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1376 my $copies = $e->json_query(
1378 select => { acp => ['id', 'circ_lib'] },
1383 fkey => 'call_number',
1387 filter => { id => $titleid },
1392 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1393 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1397 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1402 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1403 return (0) unless @$copies;
1405 # -----------------------------------------------------------------------
1406 # sort the copies into buckets based on their circ_lib proximity to
1407 # the patron's home_ou.
1408 # -----------------------------------------------------------------------
1410 my $home_org = $patron->home_ou;
1411 my $req_org = $request_lib->id;
1413 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1415 $prox_cache{$home_org} =
1416 $e->search_actor_org_unit_proximity({from_org => $home_org})
1417 unless $prox_cache{$home_org};
1418 my $home_prox = $prox_cache{$home_org};
1421 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1422 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1424 my @keys = sort { $a <=> $b } keys %buckets;
1427 if( $home_org ne $req_org ) {
1428 # -----------------------------------------------------------------------
1429 # shove the copies close to the request_lib into the primary buckets
1430 # directly before the farthest away copies. That way, they are not
1431 # given priority, but they are checked before the farthest copies.
1432 # -----------------------------------------------------------------------
1433 $prox_cache{$req_org} =
1434 $e->search_actor_org_unit_proximity({from_org => $req_org})
1435 unless $prox_cache{$req_org};
1436 my $req_prox = $prox_cache{$req_org};
1440 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1441 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1443 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1444 my $new_key = $highest_key - 0.5; # right before the farthest prox
1445 my @keys2 = sort { $a <=> $b } keys %buckets2;
1446 for my $key (@keys2) {
1447 last if $key >= $highest_key;
1448 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1452 @keys = sort { $a <=> $b } keys %buckets;
1456 for my $key (@keys) {
1457 my @cps = @{$buckets{$key}};
1459 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1461 for my $copyid (@cps) {
1463 next if $seen{$copyid};
1464 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1465 my $copy = $e->retrieve_asset_copy($copyid);
1466 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1468 unless($title) { # grab the title if we don't already have it
1469 my $vol = $e->retrieve_asset_call_number(
1470 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1471 $title = $vol->record;
1474 my @status = verify_copy_for_hold(
1475 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1477 return @status if $status[0];
1485 sub _check_volume_hold_is_possible {
1486 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1487 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1488 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1489 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1490 for my $copy ( @$copies ) {
1491 my @status = verify_copy_for_hold(
1492 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1493 return @status if $status[0];
1500 sub verify_copy_for_hold {
1501 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1502 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1503 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1504 { patron => $patron,
1505 requestor => $requestor,
1508 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1509 pickup_lib => $pickup_lib,
1510 request_lib => $request_lib,
1518 ($copy->circ_lib == $pickup_lib) and
1519 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1526 sub find_nearest_permitted_hold {
1529 my $editor = shift; # CStoreEditor object
1530 my $copy = shift; # copy to target
1531 my $user = shift; # staff
1532 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1533 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1535 my $bc = $copy->barcode;
1537 # find any existing holds that already target this copy
1538 my $old_holds = $editor->search_action_hold_request(
1539 { current_copy => $copy->id,
1540 cancel_time => undef,
1541 capture_time => undef
1545 # hold->type "R" means we need this copy
1546 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1549 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1551 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1552 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1554 # search for what should be the best holds for this copy to fulfill
1555 my $best_holds = $U->storagereq(
1556 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1557 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1559 unless(@$best_holds) {
1561 if( my $hold = $$old_holds[0] ) {
1562 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1566 $logger->info("circulator: no suitable holds found for copy $bc");
1567 return (undef, $evt);
1573 # for each potential hold, we have to run the permit script
1574 # to make sure the hold is actually permitted.
1575 for my $holdid (@$best_holds) {
1576 next unless $holdid;
1577 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1579 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1580 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1581 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1583 # see if this hold is permitted
1584 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1585 { patron_id => $hold->usr,
1588 pickup_lib => $hold->pickup_lib,
1589 request_lib => $rlib,
1600 unless( $best_hold ) { # no "good" permitted holds were found
1601 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1602 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1607 $logger->info("circulator: no suitable holds found for copy $bc");
1608 return (undef, $evt);
1611 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1613 # indicate a permitted hold was found
1614 return $best_hold if $check_only;
1616 # we've found a permitted hold. we need to "grab" the copy
1617 # to prevent re-targeted holds (next part) from re-grabbing the copy
1618 $best_hold->current_copy($copy->id);
1619 $editor->update_action_hold_request($best_hold)
1620 or return (undef, $editor->event);
1625 # re-target any other holds that already target this copy
1626 for my $old_hold (@$old_holds) {
1627 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1628 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1629 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1630 $old_hold->clear_current_copy;
1631 $old_hold->clear_prev_check_time;
1632 $editor->update_action_hold_request($old_hold)
1633 or return (undef, $editor->event);
1634 push(@retarget, $old_hold->id);
1637 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1645 __PACKAGE__->register_method(
1646 method => 'all_rec_holds',
1647 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1651 my( $self, $conn, $auth, $title_id, $args ) = @_;
1653 my $e = new_editor(authtoken=>$auth);
1654 $e->checkauth or return $e->event;
1655 $e->allowed('VIEW_HOLD') or return $e->event;
1658 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
1659 $args->{cancel_time} = undef;
1661 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1663 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1665 $resp->{metarecord_holds} = $e->search_action_hold_request(
1666 { hold_type => OILS_HOLD_TYPE_METARECORD,
1667 target => $mr_map->metarecord,
1673 $resp->{title_holds} = $e->search_action_hold_request(
1675 hold_type => OILS_HOLD_TYPE_TITLE,
1676 target => $title_id,
1680 my $vols = $e->search_asset_call_number(
1681 { record => $title_id, deleted => 'f' }, {idlist=>1});
1683 return $resp unless @$vols;
1685 $resp->{volume_holds} = $e->search_action_hold_request(
1687 hold_type => OILS_HOLD_TYPE_VOLUME,
1692 my $copies = $e->search_asset_copy(
1693 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1695 return $resp unless @$copies;
1697 $resp->{copy_holds} = $e->search_action_hold_request(
1699 hold_type => OILS_HOLD_TYPE_COPY,
1711 __PACKAGE__->register_method(
1712 method => 'uber_hold',
1714 api_name => 'open-ils.circ.hold.details.retrieve'
1718 my($self, $client, $auth, $hold_id) = @_;
1719 my $e = new_editor(authtoken=>$auth);
1720 $e->checkauth or return $e->event;
1721 $e->allowed('VIEW_HOLD') or return $e->event;
1725 my $hold = $e->retrieve_action_hold_request(
1730 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1733 ) or return $e->event;
1735 my $user = $hold->usr;
1736 $hold->usr($user->id);
1738 my $card = $e->retrieve_actor_card($user->card)
1739 or return $e->event;
1741 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1743 flesh_hold_notices([$hold], $e);
1744 flesh_hold_transits([$hold]);
1746 my $details = retrieve_hold_queue_status_impl($e, $hold);
1753 patron_first => $user->first_given_name,
1754 patron_last => $user->family_name,
1755 patron_barcode => $card->barcode,
1762 # -----------------------------------------------------
1763 # Returns the MVR object that represents what the
1765 # -----------------------------------------------------
1767 my( $e, $hold ) = @_;
1773 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1774 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1775 or return $e->event;
1776 $tid = $mr->master_record;
1778 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1779 $tid = $hold->target;
1781 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1782 $volume = $e->retrieve_asset_call_number($hold->target)
1783 or return $e->event;
1784 $tid = $volume->record;
1786 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1787 $copy = $e->retrieve_asset_copy($hold->target)
1788 or return $e->event;
1789 $volume = $e->retrieve_asset_call_number($copy->call_number)
1790 or return $e->event;
1791 $tid = $volume->record;
1794 if(!$copy and ref $hold->current_copy ) {
1795 $copy = $hold->current_copy;
1796 $hold->current_copy($copy->id);
1799 if(!$volume and $copy) {
1800 $volume = $e->retrieve_asset_call_number($copy->call_number);
1803 my $title = $e->retrieve_biblio_record_entry($tid);
1804 return ( $U->record_to_mvr($title), $volume, $copy );