1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
23 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::CStoreEditor q/:funcs/;
29 use OpenILS::Utils::PermitHold;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenILS::Const qw/:const/;
32 use OpenILS::Application::Circ::Transit;
33 use OpenILS::Application::Actor::Friends;
35 my $apputils = "OpenILS::Application::AppUtils";
41 __PACKAGE__->register_method(
42 method => "create_hold",
43 api_name => "open-ils.circ.holds.create",
45 Create a new hold for an item. From a permissions perspective,
46 the login session is used as the 'requestor' of the hold.
47 The hold recipient is determined by the 'usr' setting within
50 First we verify the requestion has holds request permissions.
51 Then we verify that the recipient is allowed to make the given hold.
52 If not, we see if the requestor has "override" capabilities. If not,
53 a permission exception is returned. If permissions allow, we cycle
54 through the set of holds objects and create.
56 If the recipient does not have permission to place multiple holds
57 on a single title and said operation is attempted, a permission
62 __PACKAGE__->register_method(
63 method => "create_hold",
64 api_name => "open-ils.circ.holds.create.override",
66 If the recipient is not allowed to receive the requested hold,
67 call this method to attempt the override
68 @see open-ils.circ.holds.create
73 my( $self, $conn, $auth, @holds ) = @_;
74 my $e = new_editor(authtoken=>$auth, xact=>1);
75 return $e->event unless $e->checkauth;
77 my $override = 1 if $self->api_name =~ /override/;
79 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
83 for my $hold (@$holds) {
88 my $requestor = $e->requestor;
89 my $recipient = $requestor;
92 if( $requestor->id ne $hold->usr ) {
93 # Make sure the requestor is allowed to place holds for
94 # the recipient if they are not the same people
95 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
96 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
99 # Now make sure the recipient is allowed to receive the specified hold
101 my $porg = $recipient->home_ou;
102 my $rid = $e->requestor->id;
103 my $t = $hold->hold_type;
105 # See if a duplicate hold already exists
107 usr => $recipient->id,
109 fulfillment_time => undef,
110 target => $hold->target,
111 cancel_time => undef,
114 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
116 my $existing = $e->search_action_hold_request($sargs);
117 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
119 if( $t eq OILS_HOLD_TYPE_METARECORD )
120 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
122 if( $t eq OILS_HOLD_TYPE_TITLE )
123 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
125 if( $t eq OILS_HOLD_TYPE_VOLUME )
126 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
128 if( $t eq OILS_HOLD_TYPE_COPY )
129 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
131 return $pevt if $pevt;
135 for my $evt (@events) {
137 my $name = $evt->{textcode};
138 return $e->event unless $e->allowed("$name.override", $porg);
145 # set the configured expire time
146 unless($hold->expire_time) {
147 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
149 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
150 $hold->expire_time($U->epoch2ISO8601($date->epoch));
154 $hold->requestor($e->requestor->id);
155 $hold->request_lib($e->requestor->ws_ou);
156 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
157 $hold = $e->create_action_hold_request($hold) or return $e->event;
162 $conn->respond_complete(1);
165 next if $U->is_true($_->frozen);
167 'open-ils.storage.action.hold_request.copy_targeter',
175 my( $self, $client, $login_session, @holds) = @_;
177 if(!@holds){return 0;}
178 my( $user, $evt ) = $apputils->checkses($login_session);
182 if(ref($holds[0]) eq 'ARRAY') {
184 } else { $holds = [ @holds ]; }
186 $logger->debug("Iterating over holds requests...");
188 for my $hold (@$holds) {
191 my $type = $hold->hold_type;
193 $logger->activity("User " . $user->id .
194 " creating new hold of type $type for user " . $hold->usr);
197 if($user->id ne $hold->usr) {
198 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
208 # am I allowed to place holds for this user?
209 if($hold->requestor ne $hold->usr) {
210 $perm = _check_request_holds_perm($user->id, $user->home_ou);
211 if($perm) { return $perm; }
214 # is this user allowed to have holds of this type?
215 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
216 return $perm if $perm;
218 #enforce the fact that the login is the one requesting the hold
219 $hold->requestor($user->id);
220 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
222 my $resp = $apputils->simplereq(
224 'open-ils.storage.direct.action.hold_request.create', $hold );
227 return OpenSRF::EX::ERROR ("Error creating hold");
234 # makes sure that a user has permission to place the type of requested hold
235 # returns the Perm exception if not allowed, returns undef if all is well
236 sub _check_holds_perm {
237 my($type, $user_id, $org_id) = @_;
241 if($evt = $apputils->check_perms(
242 $user_id, $org_id, "MR_HOLDS")) {
246 } elsif ($type eq "T") {
247 if($evt = $apputils->check_perms(
248 $user_id, $org_id, "TITLE_HOLDS")) {
252 } elsif($type eq "V") {
253 if($evt = $apputils->check_perms(
254 $user_id, $org_id, "VOLUME_HOLDS")) {
258 } elsif($type eq "C") {
259 if($evt = $apputils->check_perms(
260 $user_id, $org_id, "COPY_HOLDS")) {
268 # tests if the given user is allowed to place holds on another's behalf
269 sub _check_request_holds_perm {
272 if(my $evt = $apputils->check_perms(
273 $user_id, $org_id, "REQUEST_HOLDS")) {
278 __PACKAGE__->register_method(
279 method => "retrieve_holds_by_id",
280 api_name => "open-ils.circ.holds.retrieve_by_id",
282 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
283 different from the user, then the requestor must have VIEW_HOLD permissions.
287 sub retrieve_holds_by_id {
288 my($self, $client, $auth, $hold_id) = @_;
289 my $e = new_editor(authtoken=>$auth);
290 $e->checkauth or return $e->event;
291 $e->allowed('VIEW_HOLD') or return $e->event;
293 my $holds = $e->search_action_hold_request(
295 { id => $hold_id , fulfillment_time => undef },
296 { order_by => { ahr => "request_time" } }
300 flesh_hold_transits($holds);
301 flesh_hold_notices($holds, $e);
306 __PACKAGE__->register_method(
307 method => "retrieve_holds",
308 api_name => "open-ils.circ.holds.retrieve",
310 Retrieves all the holds, with hold transits attached, for the specified
311 user id. The login session is the requestor and if the requestor is
312 different from the user, then the requestor must have VIEW_HOLD permissions.
315 __PACKAGE__->register_method(
316 method => "retrieve_holds",
318 api_name => "open-ils.circ.holds.id_list.retrieve",
320 Retrieves all the hold ids for the specified
321 user id. The login session is the requestor and if the requestor is
322 different from the user, then the requestor must have VIEW_HOLD permissions.
326 my($self, $client, $auth, $user_id, $options) = @_;
328 my $e = new_editor(authtoken=>$auth);
329 return $e->event unless $e->checkauth;
330 $user_id = $e->requestor->id unless defined $user_id;
333 unless($user_id == $e->requestor->id) {
334 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
335 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
336 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
337 $e, $user_id, $e->requestor->id, 'hold.view');
338 return $e->event unless $allowed;
342 my $holds = $e->search_action_hold_request([
344 fulfillment_time => undef,
345 cancel_time => undef,
347 {order_by => {ahr => "request_time"}}
350 if($$options{canceled}) {
351 my $count = $$options{cancel_count} ||
352 $U->ou_ancestor_setting_value($e->requestor->ws_ou,
353 'circ.canceled_hold_display_count', $e) || 5;
355 my $canceled = $e->search_action_hold_request([
357 fulfillment_time => undef,
358 cancel_time => {'!=' => undef},
360 {order_by => {ahr => "cancel_time desc"}, limit => $count}
362 push(@$holds, @$canceled);
365 if( ! $self->api_name =~ /id_list/ ) {
366 for my $hold ( @$holds ) {
368 $e->search_action_hold_transit_copy([
370 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
375 if( $self->api_name =~ /id_list/ ) {
376 return [ map { $_->id } @$holds ];
383 __PACKAGE__->register_method(
384 method => 'user_hold_count',
385 api_name => 'open-ils.circ.hold.user.count');
387 sub user_hold_count {
388 my( $self, $conn, $auth, $userid ) = @_;
389 my $e = new_editor(authtoken=>$auth);
390 return $e->event unless $e->checkauth;
391 my $patron = $e->retrieve_actor_user($userid)
393 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
394 return __user_hold_count($self, $e, $userid);
397 sub __user_hold_count {
398 my( $self, $e, $userid ) = @_;
399 my $holds = $e->search_action_hold_request(
401 fulfillment_time => undef,
402 cancel_time => undef,
407 return scalar(@$holds);
411 __PACKAGE__->register_method(
412 method => "retrieve_holds_by_pickup_lib",
413 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
415 Retrieves all the holds, with hold transits attached, for the specified
419 __PACKAGE__->register_method(
420 method => "retrieve_holds_by_pickup_lib",
421 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
423 Retrieves all the hold ids for the specified
427 sub retrieve_holds_by_pickup_lib {
428 my($self, $client, $login_session, $ou_id) = @_;
430 #FIXME -- put an appropriate permission check here
431 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
432 # $login_session, $user_id, 'VIEW_HOLD' );
433 #return $evt if $evt;
435 my $holds = $apputils->simplereq(
437 "open-ils.cstore.direct.action.hold_request.search.atomic",
439 pickup_lib => $ou_id ,
440 fulfillment_time => undef,
443 { order_by => { ahr => "request_time" } });
446 if( ! $self->api_name =~ /id_list/ ) {
447 flesh_hold_transits($holds);
450 if( $self->api_name =~ /id_list/ ) {
451 return [ map { $_->id } @$holds ];
458 __PACKAGE__->register_method(
459 method => "uncancel_hold",
460 api_name => "open-ils.circ.hold.uncancel"
464 my($self, $client, $auth, $hold_id) = @_;
465 my $e = new_editor(authtoken=>$auth, xact=>1);
466 return $e->event unless $e->checkauth;
468 my $hold = $e->retrieve_action_hold_request($hold_id)
469 or return $e->die_event;
470 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
472 return 0 if $hold->fulfillment_time;
473 return 1 unless $hold->cancel_time;
475 # if configured to reset the request time, also reset the expire time
476 if($U->ou_ancestor_setting_value(
477 $hold->request_lib, 'circ.hold_reset_request_time_on_uncancel', $e)) {
479 $hold->request_time('now');
480 my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
482 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
483 $hold->expire_time($U->epoch2ISO8601($date->epoch));
487 $hold->clear_cancel_time;
488 $e->update_action_hold_request($hold) or return $e->die_event;
491 $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
497 __PACKAGE__->register_method(
498 method => "cancel_hold",
499 api_name => "open-ils.circ.hold.cancel",
501 Cancels the specified hold. The login session
502 is the requestor and if the requestor is different from the usr field
503 on the hold, the requestor must have CANCEL_HOLDS permissions.
504 the hold may be either the hold object or the hold id
508 my($self, $client, $auth, $holdid, $cause, $note) = @_;
510 my $e = new_editor(authtoken=>$auth, xact=>1);
511 return $e->event unless $e->checkauth;
513 my $hold = $e->retrieve_action_hold_request($holdid)
516 if( $e->requestor->id ne $hold->usr ) {
517 return $e->event unless $e->allowed('CANCEL_HOLDS');
520 return 1 if $hold->cancel_time;
522 # If the hold is captured, reset the copy status
523 if( $hold->capture_time and $hold->current_copy ) {
525 my $copy = $e->retrieve_asset_copy($hold->current_copy)
528 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
529 $logger->info("canceling hold $holdid whose item is on the holds shelf");
530 # $logger->info("setting copy to status 'reshelving' on hold cancel");
531 # $copy->status(OILS_COPY_STATUS_RESHELVING);
532 # $copy->editor($e->requestor->id);
533 # $copy->edit_date('now');
534 # $e->update_asset_copy($copy) or return $e->event;
536 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
539 $logger->warn("! canceling hold [$hid] that is in transit");
540 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
543 my $trans = $e->retrieve_action_transit_copy($transid);
544 # Leave the transit alive, but set the copy status to
545 # reshelving so it will be properly reshelved when it gets back home
547 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
548 $e->update_action_transit_copy($trans) or return $e->die_event;
554 $hold->cancel_time('now');
555 $hold->cancel_cause($cause);
556 $hold->cancel_note($note);
557 $e->update_action_hold_request($hold)
560 delete_hold_copy_maps($self, $e, $hold->id);
566 sub delete_hold_copy_maps {
571 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
573 $editor->delete_action_hold_copy_map($_)
574 or return $editor->event;
580 __PACKAGE__->register_method(
581 method => "update_hold",
582 api_name => "open-ils.circ.hold.update",
584 Updates the specified hold. The login session
585 is the requestor and if the requestor is different from the usr field
586 on the hold, the requestor must have UPDATE_HOLDS permissions.
590 my($self, $client, $auth, $hold) = @_;
592 my $e = new_editor(authtoken=>$auth, xact=>1);
593 return $e->die_event unless $e->checkauth;
595 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
596 or return $e->die_event;
598 # don't allow the user to be changed
599 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
601 if($hold->usr ne $e->requestor->id) {
602 # if the hold is for a different user, make sure the
603 # requestor has the appropriate permissions
604 my $usr = $e->retrieve_actor_user($hold->usr)
605 or return $e->die_event;
606 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
609 # --------------------------------------------------------------
610 # if the hold is on the holds shelf and the pickup lib changes,
611 # we need to create a new transit
612 # --------------------------------------------------------------
613 if( ($orig_hold->pickup_lib ne $hold->pickup_lib) and (_hold_status($e, $hold) == 4)) {
614 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
615 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
616 my $evt = transit_hold($e, $orig_hold, $hold,
617 $e->retrieve_asset_copy($hold->current_copy));
621 update_hold_if_frozen($self, $e, $hold, $orig_hold);
622 $e->update_action_hold_request($hold) or return $e->die_event;
628 my($e, $orig_hold, $hold, $copy) = @_;
629 my $src = $orig_hold->pickup_lib;
630 my $dest = $hold->pickup_lib;
632 $logger->info("putting hold into transit on pickup_lib update");
634 my $transit = Fieldmapper::action::transit_copy->new;
635 $transit->source($src);
636 $transit->dest($dest);
637 $transit->target_copy($copy->id);
638 $transit->source_send_time('now');
639 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
641 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
642 $copy->editor($e->requestor->id);
643 $copy->edit_date('now');
645 $e->create_action_transit_copy($transit) or return $e->die_event;
646 $e->update_asset_copy($copy) or return $e->die_event;
650 # if the hold is frozen, this method ensures that the hold is not "targeted",
651 # that is, it clears the current_copy and prev_check_time to essentiallly
652 # reset the hold. If it is being activated, it runs the targeter in the background
653 sub update_hold_if_frozen {
654 my($self, $e, $hold, $orig_hold) = @_;
655 return if $hold->capture_time;
657 if($U->is_true($hold->frozen)) {
658 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
659 $hold->clear_current_copy;
660 $hold->clear_prev_check_time;
663 if($U->is_true($orig_hold->frozen)) {
664 $logger->info("Running targeter on activated hold ".$hold->id);
665 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
671 __PACKAGE__->register_method(
672 method => "retrieve_hold_status",
673 api_name => "open-ils.circ.hold.status.retrieve",
675 Calculates the current status of the hold.
676 the requestor must have VIEW_HOLD permissions if the hold is for a user
677 other than the requestor.
678 Returns -1 on error (for now)
679 Returns 1 for 'waiting for copy to become available'
680 Returns 2 for 'waiting for copy capture'
681 Returns 3 for 'in transit'
682 Returns 4 for 'arrived'
685 sub retrieve_hold_status {
686 my($self, $client, $auth, $hold_id) = @_;
688 my $e = new_editor(authtoken => $auth);
689 return $e->event unless $e->checkauth;
690 my $hold = $e->retrieve_action_hold_request($hold_id)
693 if( $e->requestor->id != $hold->usr ) {
694 return $e->event unless $e->allowed('VIEW_HOLD');
697 return _hold_status($e, $hold);
703 return 1 unless $hold->current_copy;
704 return 2 unless $hold->capture_time;
706 my $copy = $hold->current_copy;
707 unless( ref $copy ) {
708 $copy = $e->retrieve_asset_copy($hold->current_copy)
712 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
713 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
720 __PACKAGE__->register_method(
721 method => "retrieve_hold_queue_stats",
722 api_name => "open-ils.circ.hold.queue_stats.retrieve",
725 Returns object with total_holds count, queue_position, potential_copies count, and status code
730 sub retrieve_hold_queue_stats {
731 my($self, $conn, $auth, $hold_id) = @_;
732 my $e = new_editor(authtoken => $auth);
733 return $e->event unless $e->checkauth;
734 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
735 if($e->requestor->id != $hold->usr) {
736 return $e->event unless $e->allowed('VIEW_HOLD');
738 return retrieve_hold_queue_status_impl($e, $hold);
741 sub retrieve_hold_queue_status_impl {
745 my $hold_ids = $e->search_action_hold_request(
747 { target => $hold->target,
748 hold_type => $hold->hold_type,
749 cancel_time => undef,
750 capture_time => undef
752 {order_by => {ahr => 'request_time asc'}}
758 for my $hid (@$hold_ids) {
760 last if $hid == $hold->id;
763 my $potentials = $e->search_action_hold_copy_map({hold => $hold->id}, {idlist => 1});
764 my $num_potentials = scalar(@$potentials);
766 my $user_org = $e->json_query({select => {au => 'home_ou'}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
767 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
768 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
771 total_holds => scalar(@$hold_ids),
772 queue_position => $qpos,
773 potential_copies => $num_potentials,
774 status => _hold_status($e, $hold),
775 estimated_wait => int($estimated_wait)
780 sub fetch_open_hold_by_current_copy {
783 my $hold = $apputils->simplereq(
785 'open-ils.cstore.direct.action.hold_request.search.atomic',
786 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
787 return $hold->[0] if ref($hold);
791 sub fetch_related_holds {
794 return $apputils->simplereq(
796 'open-ils.cstore.direct.action.hold_request.search.atomic',
797 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
801 __PACKAGE__->register_method (
802 method => "hold_pull_list",
803 api_name => "open-ils.circ.hold_pull_list.retrieve",
805 Returns a list of holds that need to be "pulled"
810 __PACKAGE__->register_method (
811 method => "hold_pull_list",
812 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
814 Returns a list of hold ID's that need to be "pulled"
821 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
822 my( $reqr, $evt ) = $U->checkses($authtoken);
825 my $org = $reqr->ws_ou || $reqr->home_ou;
826 # the perm locaiton shouldn't really matter here since holds
827 # will exist all over and VIEW_HOLDS should be universal
828 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
831 if( $self->api_name =~ /id_list/ ) {
832 return $U->storagereq(
833 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
834 $org, $limit, $offset );
836 return $U->storagereq(
837 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
838 $org, $limit, $offset );
842 __PACKAGE__->register_method (
843 method => 'fetch_hold_notify',
844 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
846 Returns a list of hold notification objects based on hold id.
847 @param authtoken The loggin session key
848 @param holdid The id of the hold whose notifications we want to retrieve
849 @return An array of hold notification objects, event on error.
853 sub fetch_hold_notify {
854 my( $self, $conn, $authtoken, $holdid ) = @_;
855 my( $requestor, $evt ) = $U->checkses($authtoken);
858 ($hold, $evt) = $U->fetch_hold($holdid);
860 ($patron, $evt) = $U->fetch_user($hold->usr);
863 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
866 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
867 return $U->cstorereq(
868 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
872 __PACKAGE__->register_method (
873 method => 'create_hold_notify',
874 api_name => 'open-ils.circ.hold_notification.create',
876 Creates a new hold notification object
877 @param authtoken The login session key
878 @param notification The hold notification object to create
879 @return ID of the new object on success, Event on error
883 sub create_hold_notify {
884 my( $self, $conn, $auth, $note ) = @_;
885 my $e = new_editor(authtoken=>$auth, xact=>1);
886 return $e->die_event unless $e->checkauth;
888 my $hold = $e->retrieve_action_hold_request($note->hold)
889 or return $e->die_event;
890 my $patron = $e->retrieve_actor_user($hold->usr)
891 or return $e->die_event;
893 return $e->die_event unless
894 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
896 $note->notify_staff($e->requestor->id);
897 $e->create_action_hold_notification($note) or return $e->die_event;
903 __PACKAGE__->register_method(
904 method => 'reset_hold',
905 api_name => 'open-ils.circ.hold.reset',
907 Un-captures and un-targets a hold, essentially returning
908 it to the state it was in directly after it was placed,
909 then attempts to re-target the hold
910 @param authtoken The login session key
911 @param holdid The id of the hold
917 my( $self, $conn, $auth, $holdid ) = @_;
919 my ($hold, $evt) = $U->fetch_hold($holdid);
921 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
923 $evt = _reset_hold($self, $reqr, $hold);
929 my ($self, $reqr, $hold) = @_;
931 my $e = new_editor(xact =>1, requestor => $reqr);
933 $logger->info("reseting hold ".$hold->id);
937 if( $hold->capture_time and $hold->current_copy ) {
939 my $copy = $e->retrieve_asset_copy($hold->current_copy)
942 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
943 $logger->info("setting copy to status 'reshelving' on hold retarget");
944 $copy->status(OILS_COPY_STATUS_RESHELVING);
945 $copy->editor($e->requestor->id);
946 $copy->edit_date('now');
947 $e->update_asset_copy($copy) or return $e->event;
949 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
951 # We don't want the copy to remain "in transit"
952 $copy->status(OILS_COPY_STATUS_RESHELVING);
953 $logger->warn("! reseting hold [$hid] that is in transit");
954 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
957 my $trans = $e->retrieve_action_transit_copy($transid);
959 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
960 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
961 $logger->info("Transit abort completed with result $evt");
962 return $evt unless "$evt" eq 1;
968 $hold->clear_capture_time;
969 $hold->clear_current_copy;
971 $e->update_action_hold_request($hold) or return $e->event;
975 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
981 __PACKAGE__->register_method(
982 method => 'fetch_open_title_holds',
983 api_name => 'open-ils.circ.open_holds.retrieve',
985 Returns a list ids of un-fulfilled holds for a given title id
986 @param authtoken The login session key
987 @param id the id of the item whose holds we want to retrieve
988 @param type The hold type - M, T, V, C
992 sub fetch_open_title_holds {
993 my( $self, $conn, $auth, $id, $type, $org ) = @_;
994 my $e = new_editor( authtoken => $auth );
995 return $e->event unless $e->checkauth;
998 $org ||= $e->requestor->ws_ou;
1000 # return $e->search_action_hold_request(
1001 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1003 # XXX make me return IDs in the future ^--
1004 my $holds = $e->search_action_hold_request(
1007 cancel_time => undef,
1009 fulfillment_time => undef
1013 flesh_hold_transits($holds);
1018 sub flesh_hold_transits {
1020 for my $hold ( @$holds ) {
1022 $apputils->simplereq(
1024 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1025 { hold => $hold->id },
1026 { order_by => { ahtc => 'id desc' }, limit => 1 }
1032 sub flesh_hold_notices {
1033 my( $holds, $e ) = @_;
1034 $e ||= new_editor();
1036 for my $hold (@$holds) {
1037 my $notices = $e->search_action_hold_notification(
1039 { hold => $hold->id },
1040 { order_by => { anh => 'notify_time desc' } },
1045 $hold->notify_count(scalar(@$notices));
1047 my $n = $e->retrieve_action_hold_notification($$notices[0])
1048 or return $e->event;
1049 $hold->notify_time($n->notify_time);
1055 __PACKAGE__->register_method(
1056 method => 'fetch_captured_holds',
1057 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1060 Returns a list of un-fulfilled holds for a given title id
1061 @param authtoken The login session key
1062 @param org The org id of the location in question
1066 __PACKAGE__->register_method(
1067 method => 'fetch_captured_holds',
1068 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1071 Returns a list ids of un-fulfilled holds for a given title id
1072 @param authtoken The login session key
1073 @param org The org id of the location in question
1077 sub fetch_captured_holds {
1078 my( $self, $conn, $auth, $org ) = @_;
1080 my $e = new_editor(authtoken => $auth);
1081 return $e->event unless $e->checkauth;
1082 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1084 $org ||= $e->requestor->ws_ou;
1086 my $hold_ids = $e->json_query(
1088 select => { ahr => ['id'] },
1093 fkey => 'current_copy'
1098 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1100 capture_time => { "!=" => undef },
1101 current_copy => { "!=" => undef },
1102 fulfillment_time => undef,
1104 cancel_time => undef,
1110 for my $hold_id (@$hold_ids) {
1111 if($self->api_name =~ /id_list/) {
1112 $conn->respond($hold_id->{id});
1116 $e->retrieve_action_hold_request([
1120 flesh_fields => {ahr => ['notifications', 'transit']},
1121 order_by => {anh => 'notify_time desc'}
1130 __PACKAGE__->register_method(
1131 method => "check_title_hold",
1132 api_name => "open-ils.circ.title_hold.is_possible",
1134 Determines if a hold were to be placed by a given user,
1135 whether or not said hold would have any potential copies
1137 @param authtoken The login session key
1138 @param params A hash of named params including:
1139 patronid - the id of the hold recipient
1140 titleid (brn) - the id of the title to be held
1141 depth - the hold range depth (defaults to 0)
1144 sub check_title_hold {
1145 my( $self, $client, $authtoken, $params ) = @_;
1147 my %params = %$params;
1148 my $titleid = $params{titleid} ||"";
1149 my $volid = $params{volume_id};
1150 my $copyid = $params{copy_id};
1151 my $mrid = $params{mrid} ||"";
1152 my $depth = $params{depth} || 0;
1153 my $pickup_lib = $params{pickup_lib};
1154 my $hold_type = $params{hold_type} || 'T';
1155 my $selection_ou = $params{selection_ou} || $pickup_lib;
1157 my $e = new_editor(authtoken=>$authtoken);
1158 return $e->event unless $e->checkauth;
1159 my $patron = $e->retrieve_actor_user($params{patronid})
1160 or return $e->event;
1162 if( $e->requestor->id ne $patron->id ) {
1163 return $e->event unless
1164 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1167 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1169 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1170 or return $e->event;
1172 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1173 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1175 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1176 # work up the tree and as soon as we find a potential copy, use that depth
1177 # also, make sure we don't go past the hard boundary if it exists
1179 # our min boundary is the greater of user-specified boundary or hard boundary
1180 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1181 $hard_boundary : $$params{depth};
1183 my $depth = $soft_boundary;
1184 while($depth >= $min_depth) {
1185 $logger->info("performing hold possibility check with soft boundary $depth");
1186 return {success => 1, depth => $depth}
1187 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1190 return {success => 0};
1192 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1193 # there is no soft boundary, enforce the hard boundary if it exists
1194 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1195 if(do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params)) {
1196 return {success => 1, depth => $hard_boundary}
1198 return {success => 0};
1202 # no boundaries defined, fall back to user specifed boundary or no boundary
1203 $logger->info("performing hold possibility check with no boundary");
1204 if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1205 return {success => 1, depth => $hard_boundary};
1207 return {success => 0};
1212 sub do_possibility_checks {
1213 my($e, $patron, $request_lib, $depth, %params) = @_;
1215 my $titleid = $params{titleid} ||"";
1216 my $volid = $params{volume_id};
1217 my $copyid = $params{copy_id};
1218 my $mrid = $params{mrid} ||"";
1219 my $pickup_lib = $params{pickup_lib};
1220 my $hold_type = $params{hold_type} || 'T';
1221 my $selection_ou = $params{selection_ou} || $pickup_lib;
1228 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1230 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1231 $volume = $e->retrieve_asset_call_number($copy->call_number)
1232 or return $e->event;
1233 $title = $e->retrieve_biblio_record_entry($volume->record)
1234 or return $e->event;
1235 return verify_copy_for_hold(
1236 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1238 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1240 $volume = $e->retrieve_asset_call_number($volid)
1241 or return $e->event;
1242 $title = $e->retrieve_biblio_record_entry($volume->record)
1243 or return $e->event;
1245 return _check_volume_hold_is_possible(
1246 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1248 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1250 return _check_title_hold_is_possible(
1251 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1253 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1255 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1256 my @recs = map { $_->source } @$maps;
1257 for my $rec (@recs) {
1258 return 1 if (_check_title_hold_is_possible(
1259 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1267 sub _check_metarecord_hold_is_possible {
1268 my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1270 my $e = new_editor();
1272 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1273 my $copies = $e->json_query(
1275 select => { acp => ['id', 'circ_lib'] },
1280 fkey => 'call_number',
1285 filter => { metarecord => $mrid }
1289 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1290 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1294 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1299 return $e->event unless defined $copies;
1300 $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1301 return 0 unless @$copies;
1303 # -----------------------------------------------------------------------
1304 # sort the copies into buckets based on their circ_lib proximity to
1305 # the patron's home_ou.
1306 # -----------------------------------------------------------------------
1308 my $home_org = $patron->home_ou;
1309 my $req_org = $request_lib->id;
1311 $prox_cache{$home_org} =
1312 $e->search_actor_org_unit_proximity({from_org => $home_org})
1313 unless $prox_cache{$home_org};
1314 my $home_prox = $prox_cache{$home_org};
1317 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1318 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1320 my @keys = sort { $a <=> $b } keys %buckets;
1323 if( $home_org ne $req_org ) {
1324 # -----------------------------------------------------------------------
1325 # shove the copies close to the request_lib into the primary buckets
1326 # directly before the farthest away copies. That way, they are not
1327 # given priority, but they are checked before the farthest copies.
1328 # -----------------------------------------------------------------------
1330 $prox_cache{$req_org} =
1331 $e->search_actor_org_unit_proximity({from_org => $req_org})
1332 unless $prox_cache{$req_org};
1333 my $req_prox = $prox_cache{$req_org};
1336 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1337 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1339 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1340 my $new_key = $highest_key - 0.5; # right before the farthest prox
1341 my @keys2 = sort { $a <=> $b } keys %buckets2;
1342 for my $key (@keys2) {
1343 last if $key >= $highest_key;
1344 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1348 @keys = sort { $a <=> $b } keys %buckets;
1351 for my $key (@keys) {
1352 my @cps = @{$buckets{$key}};
1354 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1356 for my $copyid (@cps) {
1358 next if $seen{$copyid};
1359 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1360 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1361 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1363 my $vol = $e->retrieve_asset_call_number(
1364 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1366 return 1 if verify_copy_for_hold(
1367 $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1375 sub create_ranged_org_filter {
1376 my($e, $selection_ou, $depth) = @_;
1378 # find the orgs from which this hold may be fulfilled,
1379 # based on the selection_ou and depth
1381 my $top_org = $e->search_actor_org_unit([
1382 {parent_ou => undef},
1383 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1386 return () if $depth == $top_org->ou_type->depth;
1388 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1389 %org_filter = (circ_lib => []);
1390 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1392 $logger->info("hold org filter at depth $depth and selection_ou ".
1393 "$selection_ou created list of @{$org_filter{circ_lib}}");
1399 sub _check_title_hold_is_possible {
1400 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1402 my $e = new_editor();
1403 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1405 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1406 my $copies = $e->json_query(
1408 select => { acp => ['id', 'circ_lib'] },
1413 fkey => 'call_number',
1417 filter => { id => $titleid },
1422 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1423 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1427 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1432 return $e->event unless defined $copies;
1433 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1434 return 0 unless @$copies;
1436 # -----------------------------------------------------------------------
1437 # sort the copies into buckets based on their circ_lib proximity to
1438 # the patron's home_ou.
1439 # -----------------------------------------------------------------------
1441 my $home_org = $patron->home_ou;
1442 my $req_org = $request_lib->id;
1444 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1446 $prox_cache{$home_org} =
1447 $e->search_actor_org_unit_proximity({from_org => $home_org})
1448 unless $prox_cache{$home_org};
1449 my $home_prox = $prox_cache{$home_org};
1452 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1453 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1455 my @keys = sort { $a <=> $b } keys %buckets;
1458 if( $home_org ne $req_org ) {
1459 # -----------------------------------------------------------------------
1460 # shove the copies close to the request_lib into the primary buckets
1461 # directly before the farthest away copies. That way, they are not
1462 # given priority, but they are checked before the farthest copies.
1463 # -----------------------------------------------------------------------
1464 $prox_cache{$req_org} =
1465 $e->search_actor_org_unit_proximity({from_org => $req_org})
1466 unless $prox_cache{$req_org};
1467 my $req_prox = $prox_cache{$req_org};
1471 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1472 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1474 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1475 my $new_key = $highest_key - 0.5; # right before the farthest prox
1476 my @keys2 = sort { $a <=> $b } keys %buckets2;
1477 for my $key (@keys2) {
1478 last if $key >= $highest_key;
1479 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1483 @keys = sort { $a <=> $b } keys %buckets;
1487 for my $key (@keys) {
1488 my @cps = @{$buckets{$key}};
1490 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1492 for my $copyid (@cps) {
1494 next if $seen{$copyid};
1495 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1496 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1497 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1499 unless($title) { # grab the title if we don't already have it
1500 my $vol = $e->retrieve_asset_call_number(
1501 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1502 $title = $vol->record;
1505 return 1 if verify_copy_for_hold(
1506 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1515 sub _check_volume_hold_is_possible {
1516 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1517 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1518 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1519 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1520 for my $copy ( @$copies ) {
1521 return 1 if verify_copy_for_hold(
1522 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1529 sub verify_copy_for_hold {
1530 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1531 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1532 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1533 { patron => $patron,
1534 requestor => $requestor,
1537 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1538 pickup_lib => $pickup_lib,
1539 request_lib => $request_lib,
1548 sub find_nearest_permitted_hold {
1551 my $editor = shift; # CStoreEditor object
1552 my $copy = shift; # copy to target
1553 my $user = shift; # staff
1554 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1555 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1557 my $bc = $copy->barcode;
1559 # find any existing holds that already target this copy
1560 my $old_holds = $editor->search_action_hold_request(
1561 { current_copy => $copy->id,
1562 cancel_time => undef,
1563 capture_time => undef
1567 # hold->type "R" means we need this copy
1568 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1571 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1573 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1574 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1576 # search for what should be the best holds for this copy to fulfill
1577 my $best_holds = $U->storagereq(
1578 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1579 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1581 unless(@$best_holds) {
1583 if( my $hold = $$old_holds[0] ) {
1584 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1588 $logger->info("circulator: no suitable holds found for copy $bc");
1589 return (undef, $evt);
1595 # for each potential hold, we have to run the permit script
1596 # to make sure the hold is actually permitted.
1597 for my $holdid (@$best_holds) {
1598 next unless $holdid;
1599 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1601 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1602 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1603 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1605 # see if this hold is permitted
1606 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1607 { patron_id => $hold->usr,
1610 pickup_lib => $hold->pickup_lib,
1611 request_lib => $rlib,
1622 unless( $best_hold ) { # no "good" permitted holds were found
1623 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1624 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1629 $logger->info("circulator: no suitable holds found for copy $bc");
1630 return (undef, $evt);
1633 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1635 # indicate a permitted hold was found
1636 return $best_hold if $check_only;
1638 # we've found a permitted hold. we need to "grab" the copy
1639 # to prevent re-targeted holds (next part) from re-grabbing the copy
1640 $best_hold->current_copy($copy->id);
1641 $editor->update_action_hold_request($best_hold)
1642 or return (undef, $editor->event);
1647 # re-target any other holds that already target this copy
1648 for my $old_hold (@$old_holds) {
1649 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1650 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1651 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1652 $old_hold->clear_current_copy;
1653 $old_hold->clear_prev_check_time;
1654 $editor->update_action_hold_request($old_hold)
1655 or return (undef, $editor->event);
1659 return ($best_hold, undef, $retarget);
1667 __PACKAGE__->register_method(
1668 method => 'all_rec_holds',
1669 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1673 my( $self, $conn, $auth, $title_id, $args ) = @_;
1675 my $e = new_editor(authtoken=>$auth);
1676 $e->checkauth or return $e->event;
1677 $e->allowed('VIEW_HOLD') or return $e->event;
1680 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
1681 $args->{cancel_time} = undef;
1683 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1685 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1687 $resp->{metarecord_holds} = $e->search_action_hold_request(
1688 { hold_type => OILS_HOLD_TYPE_METARECORD,
1689 target => $mr_map->metarecord,
1695 $resp->{title_holds} = $e->search_action_hold_request(
1697 hold_type => OILS_HOLD_TYPE_TITLE,
1698 target => $title_id,
1702 my $vols = $e->search_asset_call_number(
1703 { record => $title_id, deleted => 'f' }, {idlist=>1});
1705 return $resp unless @$vols;
1707 $resp->{volume_holds} = $e->search_action_hold_request(
1709 hold_type => OILS_HOLD_TYPE_VOLUME,
1714 my $copies = $e->search_asset_copy(
1715 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1717 return $resp unless @$copies;
1719 $resp->{copy_holds} = $e->search_action_hold_request(
1721 hold_type => OILS_HOLD_TYPE_COPY,
1733 __PACKAGE__->register_method(
1734 method => 'uber_hold',
1736 api_name => 'open-ils.circ.hold.details.retrieve'
1740 my($self, $client, $auth, $hold_id) = @_;
1741 my $e = new_editor(authtoken=>$auth);
1742 $e->checkauth or return $e->event;
1743 $e->allowed('VIEW_HOLD') or return $e->event;
1747 my $hold = $e->retrieve_action_hold_request(
1752 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1755 ) or return $e->event;
1757 my $user = $hold->usr;
1758 $hold->usr($user->id);
1760 my $card = $e->retrieve_actor_card($user->card)
1761 or return $e->event;
1763 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1765 flesh_hold_notices([$hold], $e);
1766 flesh_hold_transits([$hold]);
1773 status => _hold_status($e, $hold),
1774 patron_first => $user->first_given_name,
1775 patron_last => $user->family_name,
1776 patron_barcode => $card->barcode,
1782 # -----------------------------------------------------
1783 # Returns the MVR object that represents what the
1785 # -----------------------------------------------------
1787 my( $e, $hold ) = @_;
1793 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1794 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1795 or return $e->event;
1796 $tid = $mr->master_record;
1798 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1799 $tid = $hold->target;
1801 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1802 $volume = $e->retrieve_asset_call_number($hold->target)
1803 or return $e->event;
1804 $tid = $volume->record;
1806 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1807 $copy = $e->retrieve_asset_copy($hold->target)
1808 or return $e->event;
1809 $volume = $e->retrieve_asset_call_number($copy->call_number)
1810 or return $e->event;
1811 $tid = $volume->record;
1814 if(!$copy and ref $hold->current_copy ) {
1815 $copy = $hold->current_copy;
1816 $hold->current_copy($copy->id);
1819 if(!$volume and $copy) {
1820 $volume = $e->retrieve_asset_call_number($copy->call_number);
1823 my $title = $e->retrieve_biblio_record_entry($tid);
1824 return ( $U->record_to_mvr($title), $volume, $copy );