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 use OpenSRF::Utils qw/:datetime/;
37 my $apputils = "OpenILS::Application::AppUtils";
43 __PACKAGE__->register_method(
44 method => "create_hold",
45 api_name => "open-ils.circ.holds.create",
47 Create a new hold for an item. From a permissions perspective,
48 the login session is used as the 'requestor' of the hold.
49 The hold recipient is determined by the 'usr' setting within
52 First we verify the requestion has holds request permissions.
53 Then we verify that the recipient is allowed to make the given hold.
54 If not, we see if the requestor has "override" capabilities. If not,
55 a permission exception is returned. If permissions allow, we cycle
56 through the set of holds objects and create.
58 If the recipient does not have permission to place multiple holds
59 on a single title and said operation is attempted, a permission
64 __PACKAGE__->register_method(
65 method => "create_hold",
66 api_name => "open-ils.circ.holds.create.override",
68 If the recipient is not allowed to receive the requested hold,
69 call this method to attempt the override
70 @see open-ils.circ.holds.create
75 my( $self, $conn, $auth, @holds ) = @_;
76 my $e = new_editor(authtoken=>$auth, xact=>1);
77 return $e->event unless $e->checkauth;
79 my $override = 1 if $self->api_name =~ /override/;
81 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
85 for my $hold (@$holds) {
90 my $requestor = $e->requestor;
91 my $recipient = $requestor;
94 if( $requestor->id ne $hold->usr ) {
95 # Make sure the requestor is allowed to place holds for
96 # the recipient if they are not the same people
97 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
98 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
101 # Now make sure the recipient is allowed to receive the specified hold
103 my $porg = $recipient->home_ou;
104 my $rid = $e->requestor->id;
105 my $t = $hold->hold_type;
107 # See if a duplicate hold already exists
109 usr => $recipient->id,
111 fulfillment_time => undef,
112 target => $hold->target,
113 cancel_time => undef,
116 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
118 my $existing = $e->search_action_hold_request($sargs);
119 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
121 if( $t eq OILS_HOLD_TYPE_METARECORD )
122 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
124 if( $t eq OILS_HOLD_TYPE_TITLE )
125 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
127 if( $t eq OILS_HOLD_TYPE_VOLUME )
128 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
130 if( $t eq OILS_HOLD_TYPE_COPY )
131 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
133 return $pevt if $pevt;
137 for my $evt (@events) {
139 my $name = $evt->{textcode};
140 return $e->event unless $e->allowed("$name.override", $porg);
147 # set the configured expire time
148 unless($hold->expire_time) {
149 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
151 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
152 $hold->expire_time($U->epoch2ISO8601($date->epoch));
156 $hold->requestor($e->requestor->id);
157 $hold->request_lib($e->requestor->ws_ou);
158 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
159 $hold = $e->create_action_hold_request($hold) or return $e->event;
164 $conn->respond_complete(1);
167 next if $U->is_true($_->frozen);
169 'open-ils.storage.action.hold_request.copy_targeter',
177 my( $self, $client, $login_session, @holds) = @_;
179 if(!@holds){return 0;}
180 my( $user, $evt ) = $apputils->checkses($login_session);
184 if(ref($holds[0]) eq 'ARRAY') {
186 } else { $holds = [ @holds ]; }
188 $logger->debug("Iterating over holds requests...");
190 for my $hold (@$holds) {
193 my $type = $hold->hold_type;
195 $logger->activity("User " . $user->id .
196 " creating new hold of type $type for user " . $hold->usr);
199 if($user->id ne $hold->usr) {
200 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
210 # am I allowed to place holds for this user?
211 if($hold->requestor ne $hold->usr) {
212 $perm = _check_request_holds_perm($user->id, $user->home_ou);
213 if($perm) { return $perm; }
216 # is this user allowed to have holds of this type?
217 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
218 return $perm if $perm;
220 #enforce the fact that the login is the one requesting the hold
221 $hold->requestor($user->id);
222 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
224 my $resp = $apputils->simplereq(
226 'open-ils.storage.direct.action.hold_request.create', $hold );
229 return OpenSRF::EX::ERROR ("Error creating hold");
236 # makes sure that a user has permission to place the type of requested hold
237 # returns the Perm exception if not allowed, returns undef if all is well
238 sub _check_holds_perm {
239 my($type, $user_id, $org_id) = @_;
243 if($evt = $apputils->check_perms(
244 $user_id, $org_id, "MR_HOLDS")) {
248 } elsif ($type eq "T") {
249 if($evt = $apputils->check_perms(
250 $user_id, $org_id, "TITLE_HOLDS")) {
254 } elsif($type eq "V") {
255 if($evt = $apputils->check_perms(
256 $user_id, $org_id, "VOLUME_HOLDS")) {
260 } elsif($type eq "C") {
261 if($evt = $apputils->check_perms(
262 $user_id, $org_id, "COPY_HOLDS")) {
270 # tests if the given user is allowed to place holds on another's behalf
271 sub _check_request_holds_perm {
274 if(my $evt = $apputils->check_perms(
275 $user_id, $org_id, "REQUEST_HOLDS")) {
280 __PACKAGE__->register_method(
281 method => "retrieve_holds_by_id",
282 api_name => "open-ils.circ.holds.retrieve_by_id",
284 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
285 different from the user, then the requestor must have VIEW_HOLD permissions.
289 sub retrieve_holds_by_id {
290 my($self, $client, $auth, $hold_id) = @_;
291 my $e = new_editor(authtoken=>$auth);
292 $e->checkauth or return $e->event;
293 $e->allowed('VIEW_HOLD') or return $e->event;
295 my $holds = $e->search_action_hold_request(
297 { id => $hold_id , fulfillment_time => undef },
299 order_by => { ahr => "request_time" },
301 flesh_fields => {ahr => ['notes']}
306 flesh_hold_transits($holds);
307 flesh_hold_notices($holds, $e);
312 __PACKAGE__->register_method(
313 method => "retrieve_holds",
314 api_name => "open-ils.circ.holds.retrieve",
316 Retrieves all the holds, with hold transits attached, for the specified
317 user id. The login session is the requestor and if the requestor is
318 different from the user, then the requestor must have VIEW_HOLD permissions.
321 __PACKAGE__->register_method(
322 method => "retrieve_holds",
324 api_name => "open-ils.circ.holds.id_list.retrieve",
326 Retrieves all the hold ids for the specified
327 user id. The login session is the requestor and if the requestor is
328 different from the user, then the requestor must have VIEW_HOLD permissions.
332 my($self, $client, $auth, $user_id, $options) = @_;
334 my $e = new_editor(authtoken=>$auth);
335 return $e->event unless $e->checkauth;
336 $user_id = $e->requestor->id unless defined $user_id;
339 unless($user_id == $e->requestor->id) {
340 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
341 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
342 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
343 $e, $user_id, $e->requestor->id, 'hold.view');
344 return $e->event unless $allowed;
348 my $holds = $e->search_action_hold_request([
350 fulfillment_time => undef,
351 cancel_time => undef,
353 {order_by => {ahr => "request_time"}}
356 if($$options{canceled}) {
357 my $count = $$options{cancel_count} ||
358 $U->ou_ancestor_setting_value($e->requestor->ws_ou,
359 'circ.canceled_hold_display_count', $e) || 5;
361 my $canceled = $e->search_action_hold_request([
363 fulfillment_time => undef,
364 cancel_time => {'!=' => undef},
366 {order_by => {ahr => "cancel_time desc"}, limit => $count}
368 push(@$holds, @$canceled);
371 if( ! $self->api_name =~ /id_list/ ) {
372 for my $hold ( @$holds ) {
374 $e->search_action_hold_transit_copy([
376 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
381 if( $self->api_name =~ /id_list/ ) {
382 return [ map { $_->id } @$holds ];
389 __PACKAGE__->register_method(
390 method => 'user_hold_count',
391 api_name => 'open-ils.circ.hold.user.count');
393 sub user_hold_count {
394 my( $self, $conn, $auth, $userid ) = @_;
395 my $e = new_editor(authtoken=>$auth);
396 return $e->event unless $e->checkauth;
397 my $patron = $e->retrieve_actor_user($userid)
399 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
400 return __user_hold_count($self, $e, $userid);
403 sub __user_hold_count {
404 my( $self, $e, $userid ) = @_;
405 my $holds = $e->search_action_hold_request(
407 fulfillment_time => undef,
408 cancel_time => undef,
413 return scalar(@$holds);
417 __PACKAGE__->register_method(
418 method => "retrieve_holds_by_pickup_lib",
419 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
421 Retrieves all the holds, with hold transits attached, for the specified
425 __PACKAGE__->register_method(
426 method => "retrieve_holds_by_pickup_lib",
427 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
429 Retrieves all the hold ids for the specified
433 sub retrieve_holds_by_pickup_lib {
434 my($self, $client, $login_session, $ou_id) = @_;
436 #FIXME -- put an appropriate permission check here
437 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
438 # $login_session, $user_id, 'VIEW_HOLD' );
439 #return $evt if $evt;
441 my $holds = $apputils->simplereq(
443 "open-ils.cstore.direct.action.hold_request.search.atomic",
445 pickup_lib => $ou_id ,
446 fulfillment_time => undef,
449 { order_by => { ahr => "request_time" } });
452 if( ! $self->api_name =~ /id_list/ ) {
453 flesh_hold_transits($holds);
456 if( $self->api_name =~ /id_list/ ) {
457 return [ map { $_->id } @$holds ];
464 __PACKAGE__->register_method(
465 method => "uncancel_hold",
466 api_name => "open-ils.circ.hold.uncancel"
470 my($self, $client, $auth, $hold_id) = @_;
471 my $e = new_editor(authtoken=>$auth, xact=>1);
472 return $e->event unless $e->checkauth;
474 my $hold = $e->retrieve_action_hold_request($hold_id)
475 or return $e->die_event;
476 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
478 return 0 if $hold->fulfillment_time;
479 return 1 unless $hold->cancel_time;
481 # if configured to reset the request time, also reset the expire time
482 if($U->ou_ancestor_setting_value(
483 $hold->request_lib, 'circ.hold_reset_request_time_on_uncancel', $e)) {
485 $hold->request_time('now');
486 my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
488 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
489 $hold->expire_time($U->epoch2ISO8601($date->epoch));
493 $hold->clear_cancel_time;
494 $e->update_action_hold_request($hold) or return $e->die_event;
497 $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
503 __PACKAGE__->register_method(
504 method => "cancel_hold",
505 api_name => "open-ils.circ.hold.cancel",
507 Cancels the specified hold. The login session
508 is the requestor and if the requestor is different from the usr field
509 on the hold, the requestor must have CANCEL_HOLDS permissions.
510 the hold may be either the hold object or the hold id
514 my($self, $client, $auth, $holdid, $cause, $note) = @_;
516 my $e = new_editor(authtoken=>$auth, xact=>1);
517 return $e->event unless $e->checkauth;
519 my $hold = $e->retrieve_action_hold_request($holdid)
522 if( $e->requestor->id ne $hold->usr ) {
523 return $e->event unless $e->allowed('CANCEL_HOLDS');
526 return 1 if $hold->cancel_time;
528 # If the hold is captured, reset the copy status
529 if( $hold->capture_time and $hold->current_copy ) {
531 my $copy = $e->retrieve_asset_copy($hold->current_copy)
534 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
535 $logger->info("canceling hold $holdid whose item is on the holds shelf");
536 # $logger->info("setting copy to status 'reshelving' on hold cancel");
537 # $copy->status(OILS_COPY_STATUS_RESHELVING);
538 # $copy->editor($e->requestor->id);
539 # $copy->edit_date('now');
540 # $e->update_asset_copy($copy) or return $e->event;
542 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
545 $logger->warn("! canceling hold [$hid] that is in transit");
546 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
549 my $trans = $e->retrieve_action_transit_copy($transid);
550 # Leave the transit alive, but set the copy status to
551 # reshelving so it will be properly reshelved when it gets back home
553 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
554 $e->update_action_transit_copy($trans) or return $e->die_event;
560 $hold->cancel_time('now');
561 $hold->cancel_cause($cause);
562 $hold->cancel_note($note);
563 $e->update_action_hold_request($hold)
566 delete_hold_copy_maps($self, $e, $hold->id);
572 sub delete_hold_copy_maps {
577 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
579 $editor->delete_action_hold_copy_map($_)
580 or return $editor->event;
586 __PACKAGE__->register_method(
587 method => "update_hold",
588 api_name => "open-ils.circ.hold.update",
590 Updates the specified hold. The login session
591 is the requestor and if the requestor is different from the usr field
592 on the hold, the requestor must have UPDATE_HOLDS permissions.
596 my($self, $client, $auth, $hold) = @_;
598 my $e = new_editor(authtoken=>$auth, xact=>1);
599 return $e->die_event unless $e->checkauth;
601 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
602 or return $e->die_event;
604 # don't allow the user to be changed
605 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
607 if($hold->usr ne $e->requestor->id) {
608 # if the hold is for a different user, make sure the
609 # requestor has the appropriate permissions
610 my $usr = $e->retrieve_actor_user($hold->usr)
611 or return $e->die_event;
612 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
615 # --------------------------------------------------------------
616 # if the hold is on the holds shelf and the pickup lib changes,
617 # we need to create a new transit
618 # --------------------------------------------------------------
619 if( ($orig_hold->pickup_lib ne $hold->pickup_lib) and (_hold_status($e, $hold) == 4)) {
620 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
621 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
622 my $evt = transit_hold($e, $orig_hold, $hold,
623 $e->retrieve_asset_copy($hold->current_copy));
627 update_hold_if_frozen($self, $e, $hold, $orig_hold);
628 $e->update_action_hold_request($hold) or return $e->die_event;
634 my($e, $orig_hold, $hold, $copy) = @_;
635 my $src = $orig_hold->pickup_lib;
636 my $dest = $hold->pickup_lib;
638 $logger->info("putting hold into transit on pickup_lib update");
640 my $transit = Fieldmapper::action::transit_copy->new;
641 $transit->source($src);
642 $transit->dest($dest);
643 $transit->target_copy($copy->id);
644 $transit->source_send_time('now');
645 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
647 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
648 $copy->editor($e->requestor->id);
649 $copy->edit_date('now');
651 $e->create_action_transit_copy($transit) or return $e->die_event;
652 $e->update_asset_copy($copy) or return $e->die_event;
656 # if the hold is frozen, this method ensures that the hold is not "targeted",
657 # that is, it clears the current_copy and prev_check_time to essentiallly
658 # reset the hold. If it is being activated, it runs the targeter in the background
659 sub update_hold_if_frozen {
660 my($self, $e, $hold, $orig_hold) = @_;
661 return if $hold->capture_time;
663 if($U->is_true($hold->frozen)) {
664 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
665 $hold->clear_current_copy;
666 $hold->clear_prev_check_time;
669 if($U->is_true($orig_hold->frozen)) {
670 $logger->info("Running targeter on activated hold ".$hold->id);
671 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
675 __PACKAGE__->register_method(
676 method => "hold_note_CUD",
677 api_name => "open-ils.circ.hold_request.note.cud");
680 my($self, $conn, $auth, $note) = @_;
682 my $e = new_editor(authtoken => $auth, xact => 1);
683 return $e->die_event unless $e->checkauth;
685 my $hold = $e->retrieve_action_hold_request($note->hold)
686 or return $e->die_event;
688 if($hold->usr ne $e->requestor->id) {
689 my $usr = $e->retrieve_actor_user($hold->usr);
690 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
691 $note->staff('t') if $note->isnew;
695 $e->create_action_hold_request_note($note) or return $e->die_event;
696 } elsif($note->ischanged) {
697 $e->update_action_hold_request_note($note) or return $e->die_event;
698 } elsif($note->isdeleted) {
699 $e->delete_action_hold_request_note($note) or return $e->die_event;
708 __PACKAGE__->register_method(
709 method => "retrieve_hold_status",
710 api_name => "open-ils.circ.hold.status.retrieve",
712 Calculates the current status of the hold.
713 the requestor must have VIEW_HOLD permissions if the hold is for a user
714 other than the requestor.
715 Returns -1 on error (for now)
716 Returns 1 for 'waiting for copy to become available'
717 Returns 2 for 'waiting for copy capture'
718 Returns 3 for 'in transit'
719 Returns 4 for 'arrived'
720 Returns 5 for 'hold-shelf-delay'
723 sub retrieve_hold_status {
724 my($self, $client, $auth, $hold_id) = @_;
726 my $e = new_editor(authtoken => $auth);
727 return $e->event unless $e->checkauth;
728 my $hold = $e->retrieve_action_hold_request($hold_id)
731 if( $e->requestor->id != $hold->usr ) {
732 return $e->event unless $e->allowed('VIEW_HOLD');
735 return _hold_status($e, $hold);
741 return 1 unless $hold->current_copy;
742 return 2 unless $hold->capture_time;
744 my $copy = $hold->current_copy;
745 unless( ref $copy ) {
746 $copy = $e->retrieve_asset_copy($hold->current_copy)
750 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
752 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
754 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
755 return 4 unless $hs_wait_interval;
757 # if a hold_shelf_status_delay interval is defined and start_time plus
758 # the interval is greater than now, consider the hold to be in the virtual
759 # "on its way to the holds shelf" status. Return 5.
761 my $transit = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
762 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
763 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clense_ISO8601($start_time));
764 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
766 return 5 if $end_time > DateTime->now;
775 __PACKAGE__->register_method(
776 method => "retrieve_hold_queue_stats",
777 api_name => "open-ils.circ.hold.queue_stats.retrieve",
780 Returns object with total_holds count, queue_position, potential_copies count, and status code
785 sub retrieve_hold_queue_stats {
786 my($self, $conn, $auth, $hold_id) = @_;
787 my $e = new_editor(authtoken => $auth);
788 return $e->event unless $e->checkauth;
789 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
790 if($e->requestor->id != $hold->usr) {
791 return $e->event unless $e->allowed('VIEW_HOLD');
793 return retrieve_hold_queue_status_impl($e, $hold);
796 sub retrieve_hold_queue_status_impl {
800 my $hold_ids = $e->search_action_hold_request(
802 { target => $hold->target,
803 hold_type => $hold->hold_type,
804 cancel_time => undef,
805 fulfillment_time => undef
807 {order_by => {ahr => 'request_time asc'}}
813 for my $hid (@$hold_ids) {
814 last if $hid == $hold->id;
818 my $potentials = $e->search_action_hold_copy_map({hold => $hold->id}, {idlist => 1});
819 my $num_potentials = scalar(@$potentials);
821 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
822 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
823 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
826 total_holds => scalar(@$hold_ids),
827 queue_position => $qpos,
828 potential_copies => $num_potentials,
829 status => _hold_status($e, $hold),
830 estimated_wait => int($estimated_wait)
835 sub fetch_open_hold_by_current_copy {
838 my $hold = $apputils->simplereq(
840 'open-ils.cstore.direct.action.hold_request.search.atomic',
841 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
842 return $hold->[0] if ref($hold);
846 sub fetch_related_holds {
849 return $apputils->simplereq(
851 'open-ils.cstore.direct.action.hold_request.search.atomic',
852 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
856 __PACKAGE__->register_method (
857 method => "hold_pull_list",
858 api_name => "open-ils.circ.hold_pull_list.retrieve",
860 Returns a list of holds that need to be "pulled"
865 __PACKAGE__->register_method (
866 method => "hold_pull_list",
867 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
869 Returns a list of hold ID's that need to be "pulled"
876 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
877 my( $reqr, $evt ) = $U->checkses($authtoken);
880 my $org = $reqr->ws_ou || $reqr->home_ou;
881 # the perm locaiton shouldn't really matter here since holds
882 # will exist all over and VIEW_HOLDS should be universal
883 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
886 if( $self->api_name =~ /id_list/ ) {
887 return $U->storagereq(
888 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
889 $org, $limit, $offset );
891 return $U->storagereq(
892 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
893 $org, $limit, $offset );
897 __PACKAGE__->register_method (
898 method => 'fetch_hold_notify',
899 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
901 Returns a list of hold notification objects based on hold id.
902 @param authtoken The loggin session key
903 @param holdid The id of the hold whose notifications we want to retrieve
904 @return An array of hold notification objects, event on error.
908 sub fetch_hold_notify {
909 my( $self, $conn, $authtoken, $holdid ) = @_;
910 my( $requestor, $evt ) = $U->checkses($authtoken);
913 ($hold, $evt) = $U->fetch_hold($holdid);
915 ($patron, $evt) = $U->fetch_user($hold->usr);
918 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
921 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
922 return $U->cstorereq(
923 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
927 __PACKAGE__->register_method (
928 method => 'create_hold_notify',
929 api_name => 'open-ils.circ.hold_notification.create',
931 Creates a new hold notification object
932 @param authtoken The login session key
933 @param notification The hold notification object to create
934 @return ID of the new object on success, Event on error
938 sub create_hold_notify {
939 my( $self, $conn, $auth, $note ) = @_;
940 my $e = new_editor(authtoken=>$auth, xact=>1);
941 return $e->die_event unless $e->checkauth;
943 my $hold = $e->retrieve_action_hold_request($note->hold)
944 or return $e->die_event;
945 my $patron = $e->retrieve_actor_user($hold->usr)
946 or return $e->die_event;
948 return $e->die_event unless
949 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
951 $note->notify_staff($e->requestor->id);
952 $e->create_action_hold_notification($note) or return $e->die_event;
958 __PACKAGE__->register_method(
959 method => 'reset_hold',
960 api_name => 'open-ils.circ.hold.reset',
962 Un-captures and un-targets a hold, essentially returning
963 it to the state it was in directly after it was placed,
964 then attempts to re-target the hold
965 @param authtoken The login session key
966 @param holdid The id of the hold
972 my( $self, $conn, $auth, $holdid ) = @_;
974 my ($hold, $evt) = $U->fetch_hold($holdid);
976 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
978 $evt = _reset_hold($self, $reqr, $hold);
984 __PACKAGE__->register_method(
985 method => 'reset_hold_batch',
986 api_name => 'open-ils.circ.hold.reset.batch'
989 sub reset_hold_batch {
990 my($self, $conn, $auth, $hold_ids) = @_;
992 my $e = new_editor(authtoken => $auth);
993 return $e->event unless $e->checkauth;
995 for my $hold_id ($hold_ids) {
997 my $hold = $e->retrieve_action_hold_request(
998 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1001 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1002 _reset_hold($self, $e->requestor, $hold);
1010 my ($self, $reqr, $hold) = @_;
1012 my $e = new_editor(xact =>1, requestor => $reqr);
1014 $logger->info("reseting hold ".$hold->id);
1016 my $hid = $hold->id;
1018 if( $hold->capture_time and $hold->current_copy ) {
1020 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1021 or return $e->event;
1023 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1024 $logger->info("setting copy to status 'reshelving' on hold retarget");
1025 $copy->status(OILS_COPY_STATUS_RESHELVING);
1026 $copy->editor($e->requestor->id);
1027 $copy->edit_date('now');
1028 $e->update_asset_copy($copy) or return $e->event;
1030 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1032 # We don't want the copy to remain "in transit"
1033 $copy->status(OILS_COPY_STATUS_RESHELVING);
1034 $logger->warn("! reseting hold [$hid] that is in transit");
1035 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1038 my $trans = $e->retrieve_action_transit_copy($transid);
1040 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1041 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1042 $logger->info("Transit abort completed with result $evt");
1043 return $evt unless "$evt" eq 1;
1049 $hold->clear_capture_time;
1050 $hold->clear_current_copy;
1052 $e->update_action_hold_request($hold) or return $e->event;
1056 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1062 __PACKAGE__->register_method(
1063 method => 'fetch_open_title_holds',
1064 api_name => 'open-ils.circ.open_holds.retrieve',
1066 Returns a list ids of un-fulfilled holds for a given title id
1067 @param authtoken The login session key
1068 @param id the id of the item whose holds we want to retrieve
1069 @param type The hold type - M, T, V, C
1073 sub fetch_open_title_holds {
1074 my( $self, $conn, $auth, $id, $type, $org ) = @_;
1075 my $e = new_editor( authtoken => $auth );
1076 return $e->event unless $e->checkauth;
1079 $org ||= $e->requestor->ws_ou;
1081 # return $e->search_action_hold_request(
1082 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1084 # XXX make me return IDs in the future ^--
1085 my $holds = $e->search_action_hold_request(
1088 cancel_time => undef,
1090 fulfillment_time => undef
1094 flesh_hold_transits($holds);
1099 sub flesh_hold_transits {
1101 for my $hold ( @$holds ) {
1103 $apputils->simplereq(
1105 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1106 { hold => $hold->id },
1107 { order_by => { ahtc => 'id desc' }, limit => 1 }
1113 sub flesh_hold_notices {
1114 my( $holds, $e ) = @_;
1115 $e ||= new_editor();
1117 for my $hold (@$holds) {
1118 my $notices = $e->search_action_hold_notification(
1120 { hold => $hold->id },
1121 { order_by => { anh => 'notify_time desc' } },
1126 $hold->notify_count(scalar(@$notices));
1128 my $n = $e->retrieve_action_hold_notification($$notices[0])
1129 or return $e->event;
1130 $hold->notify_time($n->notify_time);
1136 __PACKAGE__->register_method(
1137 method => 'fetch_captured_holds',
1138 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1141 Returns a list of un-fulfilled holds for a given title id
1142 @param authtoken The login session key
1143 @param org The org id of the location in question
1147 __PACKAGE__->register_method(
1148 method => 'fetch_captured_holds',
1149 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1152 Returns a list ids of un-fulfilled holds for a given title id
1153 @param authtoken The login session key
1154 @param org The org id of the location in question
1158 sub fetch_captured_holds {
1159 my( $self, $conn, $auth, $org ) = @_;
1161 my $e = new_editor(authtoken => $auth);
1162 return $e->event unless $e->checkauth;
1163 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1165 $org ||= $e->requestor->ws_ou;
1167 my $hold_ids = $e->json_query(
1169 select => { ahr => ['id'] },
1174 fkey => 'current_copy'
1179 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1181 capture_time => { "!=" => undef },
1182 current_copy => { "!=" => undef },
1183 fulfillment_time => undef,
1185 cancel_time => undef,
1191 for my $hold_id (@$hold_ids) {
1192 if($self->api_name =~ /id_list/) {
1193 $conn->respond($hold_id->{id});
1197 $e->retrieve_action_hold_request([
1201 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1202 order_by => {anh => 'notify_time desc'}
1211 __PACKAGE__->register_method(
1212 method => "check_title_hold",
1213 api_name => "open-ils.circ.title_hold.is_possible",
1215 Determines if a hold were to be placed by a given user,
1216 whether or not said hold would have any potential copies
1218 @param authtoken The login session key
1219 @param params A hash of named params including:
1220 patronid - the id of the hold recipient
1221 titleid (brn) - the id of the title to be held
1222 depth - the hold range depth (defaults to 0)
1225 sub check_title_hold {
1226 my( $self, $client, $authtoken, $params ) = @_;
1228 my %params = %$params;
1229 my $titleid = $params{titleid} ||"";
1230 my $volid = $params{volume_id};
1231 my $copyid = $params{copy_id};
1232 my $mrid = $params{mrid} ||"";
1233 my $depth = $params{depth} || 0;
1234 my $pickup_lib = $params{pickup_lib};
1235 my $hold_type = $params{hold_type} || 'T';
1236 my $selection_ou = $params{selection_ou} || $pickup_lib;
1238 my $e = new_editor(authtoken=>$authtoken);
1239 return $e->event unless $e->checkauth;
1240 my $patron = $e->retrieve_actor_user($params{patronid})
1241 or return $e->event;
1243 if( $e->requestor->id ne $patron->id ) {
1244 return $e->event unless
1245 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1248 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1250 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1251 or return $e->event;
1253 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1254 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1256 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1257 # work up the tree and as soon as we find a potential copy, use that depth
1258 # also, make sure we don't go past the hard boundary if it exists
1260 # our min boundary is the greater of user-specified boundary or hard boundary
1261 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1262 $hard_boundary : $$params{depth};
1264 my $depth = $soft_boundary;
1265 while($depth >= $min_depth) {
1266 $logger->info("performing hold possibility check with soft boundary $depth");
1267 my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1268 return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1271 return {success => 0};
1273 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1274 # there is no soft boundary, enforce the hard boundary if it exists
1275 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1276 my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1278 return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1280 return {success => 0};
1284 # no boundaries defined, fall back to user specifed boundary or no boundary
1285 $logger->info("performing hold possibility check with no boundary");
1286 my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1288 return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1290 return {success => 0};
1295 sub do_possibility_checks {
1296 my($e, $patron, $request_lib, $depth, %params) = @_;
1298 my $titleid = $params{titleid} ||"";
1299 my $volid = $params{volume_id};
1300 my $copyid = $params{copy_id};
1301 my $mrid = $params{mrid} ||"";
1302 my $pickup_lib = $params{pickup_lib};
1303 my $hold_type = $params{hold_type} || 'T';
1304 my $selection_ou = $params{selection_ou} || $pickup_lib;
1311 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1313 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1314 $volume = $e->retrieve_asset_call_number($copy->call_number)
1315 or return $e->event;
1316 $title = $e->retrieve_biblio_record_entry($volume->record)
1317 or return $e->event;
1318 return verify_copy_for_hold(
1319 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1321 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1323 $volume = $e->retrieve_asset_call_number($volid)
1324 or return $e->event;
1325 $title = $e->retrieve_biblio_record_entry($volume->record)
1326 or return $e->event;
1328 return _check_volume_hold_is_possible(
1329 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1331 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1333 return _check_title_hold_is_possible(
1334 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1336 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1338 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1339 my @recs = map { $_->source } @$maps;
1340 for my $rec (@recs) {
1341 my @status = _check_title_hold_is_possible(
1342 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1343 return @status if $status[1];
1350 sub create_ranged_org_filter {
1351 my($e, $selection_ou, $depth) = @_;
1353 # find the orgs from which this hold may be fulfilled,
1354 # based on the selection_ou and depth
1356 my $top_org = $e->search_actor_org_unit([
1357 {parent_ou => undef},
1358 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1361 return () if $depth == $top_org->ou_type->depth;
1363 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1364 %org_filter = (circ_lib => []);
1365 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1367 $logger->info("hold org filter at depth $depth and selection_ou ".
1368 "$selection_ou created list of @{$org_filter{circ_lib}}");
1374 sub _check_title_hold_is_possible {
1375 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1377 my $e = new_editor();
1378 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1380 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1381 my $copies = $e->json_query(
1383 select => { acp => ['id', 'circ_lib'] },
1388 fkey => 'call_number',
1392 filter => { id => $titleid },
1397 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1398 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1402 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1407 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1408 return (0) unless @$copies;
1410 # -----------------------------------------------------------------------
1411 # sort the copies into buckets based on their circ_lib proximity to
1412 # the patron's home_ou.
1413 # -----------------------------------------------------------------------
1415 my $home_org = $patron->home_ou;
1416 my $req_org = $request_lib->id;
1418 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1420 $prox_cache{$home_org} =
1421 $e->search_actor_org_unit_proximity({from_org => $home_org})
1422 unless $prox_cache{$home_org};
1423 my $home_prox = $prox_cache{$home_org};
1426 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1427 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1429 my @keys = sort { $a <=> $b } keys %buckets;
1432 if( $home_org ne $req_org ) {
1433 # -----------------------------------------------------------------------
1434 # shove the copies close to the request_lib into the primary buckets
1435 # directly before the farthest away copies. That way, they are not
1436 # given priority, but they are checked before the farthest copies.
1437 # -----------------------------------------------------------------------
1438 $prox_cache{$req_org} =
1439 $e->search_actor_org_unit_proximity({from_org => $req_org})
1440 unless $prox_cache{$req_org};
1441 my $req_prox = $prox_cache{$req_org};
1445 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1446 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1448 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1449 my $new_key = $highest_key - 0.5; # right before the farthest prox
1450 my @keys2 = sort { $a <=> $b } keys %buckets2;
1451 for my $key (@keys2) {
1452 last if $key >= $highest_key;
1453 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1457 @keys = sort { $a <=> $b } keys %buckets;
1461 for my $key (@keys) {
1462 my @cps = @{$buckets{$key}};
1464 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1466 for my $copyid (@cps) {
1468 next if $seen{$copyid};
1469 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1470 my $copy = $e->retrieve_asset_copy($copyid);
1471 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1473 unless($title) { # grab the title if we don't already have it
1474 my $vol = $e->retrieve_asset_call_number(
1475 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1476 $title = $vol->record;
1479 my @status = verify_copy_for_hold(
1480 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1482 return @status if $status[0];
1490 sub _check_volume_hold_is_possible {
1491 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1492 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1493 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1494 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1495 for my $copy ( @$copies ) {
1496 my @status = verify_copy_for_hold(
1497 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1498 return @status if $status[0];
1505 sub verify_copy_for_hold {
1506 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1507 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1508 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1509 { patron => $patron,
1510 requestor => $requestor,
1513 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1514 pickup_lib => $pickup_lib,
1515 request_lib => $request_lib,
1523 ($copy->circ_lib == $pickup_lib) and
1524 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1531 sub find_nearest_permitted_hold {
1534 my $editor = shift; # CStoreEditor object
1535 my $copy = shift; # copy to target
1536 my $user = shift; # staff
1537 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1538 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1540 my $bc = $copy->barcode;
1542 # find any existing holds that already target this copy
1543 my $old_holds = $editor->search_action_hold_request(
1544 { current_copy => $copy->id,
1545 cancel_time => undef,
1546 capture_time => undef
1550 # hold->type "R" means we need this copy
1551 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1554 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1556 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1557 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1559 # search for what should be the best holds for this copy to fulfill
1560 my $best_holds = $U->storagereq(
1561 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1562 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1564 unless(@$best_holds) {
1566 if( my $hold = $$old_holds[0] ) {
1567 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1571 $logger->info("circulator: no suitable holds found for copy $bc");
1572 return (undef, $evt);
1578 # for each potential hold, we have to run the permit script
1579 # to make sure the hold is actually permitted.
1580 for my $holdid (@$best_holds) {
1581 next unless $holdid;
1582 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1584 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1585 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1586 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1588 # see if this hold is permitted
1589 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1590 { patron_id => $hold->usr,
1593 pickup_lib => $hold->pickup_lib,
1594 request_lib => $rlib,
1605 unless( $best_hold ) { # no "good" permitted holds were found
1606 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1607 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1612 $logger->info("circulator: no suitable holds found for copy $bc");
1613 return (undef, $evt);
1616 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1618 # indicate a permitted hold was found
1619 return $best_hold if $check_only;
1621 # we've found a permitted hold. we need to "grab" the copy
1622 # to prevent re-targeted holds (next part) from re-grabbing the copy
1623 $best_hold->current_copy($copy->id);
1624 $editor->update_action_hold_request($best_hold)
1625 or return (undef, $editor->event);
1630 # re-target any other holds that already target this copy
1631 for my $old_hold (@$old_holds) {
1632 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1633 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1634 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1635 $old_hold->clear_current_copy;
1636 $old_hold->clear_prev_check_time;
1637 $editor->update_action_hold_request($old_hold)
1638 or return (undef, $editor->event);
1639 push(@retarget, $old_hold->id);
1642 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1650 __PACKAGE__->register_method(
1651 method => 'all_rec_holds',
1652 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1656 my( $self, $conn, $auth, $title_id, $args ) = @_;
1658 my $e = new_editor(authtoken=>$auth);
1659 $e->checkauth or return $e->event;
1660 $e->allowed('VIEW_HOLD') or return $e->event;
1663 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
1664 $args->{cancel_time} = undef;
1666 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1668 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1670 $resp->{metarecord_holds} = $e->search_action_hold_request(
1671 { hold_type => OILS_HOLD_TYPE_METARECORD,
1672 target => $mr_map->metarecord,
1678 $resp->{title_holds} = $e->search_action_hold_request(
1680 hold_type => OILS_HOLD_TYPE_TITLE,
1681 target => $title_id,
1685 my $vols = $e->search_asset_call_number(
1686 { record => $title_id, deleted => 'f' }, {idlist=>1});
1688 return $resp unless @$vols;
1690 $resp->{volume_holds} = $e->search_action_hold_request(
1692 hold_type => OILS_HOLD_TYPE_VOLUME,
1697 my $copies = $e->search_asset_copy(
1698 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1700 return $resp unless @$copies;
1702 $resp->{copy_holds} = $e->search_action_hold_request(
1704 hold_type => OILS_HOLD_TYPE_COPY,
1716 __PACKAGE__->register_method(
1717 method => 'uber_hold',
1719 api_name => 'open-ils.circ.hold.details.retrieve'
1723 my($self, $client, $auth, $hold_id) = @_;
1724 my $e = new_editor(authtoken=>$auth);
1725 $e->checkauth or return $e->event;
1726 $e->allowed('VIEW_HOLD') or return $e->event;
1730 my $hold = $e->retrieve_action_hold_request(
1735 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1738 ) or return $e->event;
1740 my $user = $hold->usr;
1741 $hold->usr($user->id);
1743 my $card = $e->retrieve_actor_card($user->card)
1744 or return $e->event;
1746 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1748 flesh_hold_notices([$hold], $e);
1749 flesh_hold_transits([$hold]);
1751 my $details = retrieve_hold_queue_status_impl($e, $hold);
1758 patron_first => $user->first_given_name,
1759 patron_last => $user->family_name,
1760 patron_barcode => $card->barcode,
1767 # -----------------------------------------------------
1768 # Returns the MVR object that represents what the
1770 # -----------------------------------------------------
1772 my( $e, $hold ) = @_;
1778 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1779 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1780 or return $e->event;
1781 $tid = $mr->master_record;
1783 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1784 $tid = $hold->target;
1786 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1787 $volume = $e->retrieve_asset_call_number($hold->target)
1788 or return $e->event;
1789 $tid = $volume->record;
1791 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1792 $copy = $e->retrieve_asset_copy($hold->target)
1793 or return $e->event;
1794 $volume = $e->retrieve_asset_call_number($copy->call_number)
1795 or return $e->event;
1796 $tid = $volume->record;
1799 if(!$copy and ref $hold->current_copy ) {
1800 $copy = $hold->current_copy;
1801 $hold->current_copy($copy->id);
1804 if(!$volume and $copy) {
1805 $volume = $e->retrieve_asset_call_number($copy->call_number);
1808 my $title = $e->retrieve_biblio_record_entry($tid);
1809 return ( $U->record_to_mvr($title), $volume, $copy );