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, $hold ) = @_;
76 my $e = new_editor(authtoken=>$auth, xact=>1);
77 return $e->event unless $e->checkauth;
79 return -1 unless $hold;
80 my $override = 1 if $self->api_name =~ /override/;
84 my $requestor = $e->requestor;
85 my $recipient = $requestor;
87 if( $requestor->id ne $hold->usr ) {
88 # Make sure the requestor is allowed to place holds for
89 # the recipient if they are not the same people
90 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
91 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
94 # Now make sure the recipient is allowed to receive the specified hold
96 my $porg = $recipient->home_ou;
97 my $rid = $e->requestor->id;
98 my $t = $hold->hold_type;
100 # See if a duplicate hold already exists
102 usr => $recipient->id,
104 fulfillment_time => undef,
105 target => $hold->target,
106 cancel_time => undef,
109 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
111 my $existing = $e->search_action_hold_request($sargs);
112 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
114 if( $t eq OILS_HOLD_TYPE_METARECORD )
115 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
117 if( $t eq OILS_HOLD_TYPE_TITLE )
118 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
120 if( $t eq OILS_HOLD_TYPE_VOLUME )
121 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
123 if( $t eq OILS_HOLD_TYPE_COPY )
124 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
126 return $pevt if $pevt;
130 for my $evt (@events) {
132 my $name = $evt->{textcode};
133 return $e->event unless $e->allowed("$name.override", $porg);
140 # set the configured expire time
141 unless($hold->expire_time) {
142 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
144 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
145 $hold->expire_time($U->epoch2ISO8601($date->epoch));
149 $hold->requestor($e->requestor->id);
150 $hold->request_lib($e->requestor->ws_ou);
151 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
152 $hold = $e->create_action_hold_request($hold) or return $e->event;
156 $conn->respond_complete($hold->id);
159 'open-ils.storage.action.hold_request.copy_targeter',
160 undef, $hold->id ) unless $U->is_true($hold->frozen);
166 my( $self, $client, $login_session, @holds) = @_;
168 if(!@holds){return 0;}
169 my( $user, $evt ) = $apputils->checkses($login_session);
173 if(ref($holds[0]) eq 'ARRAY') {
175 } else { $holds = [ @holds ]; }
177 $logger->debug("Iterating over holds requests...");
179 for my $hold (@$holds) {
182 my $type = $hold->hold_type;
184 $logger->activity("User " . $user->id .
185 " creating new hold of type $type for user " . $hold->usr);
188 if($user->id ne $hold->usr) {
189 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
199 # am I allowed to place holds for this user?
200 if($hold->requestor ne $hold->usr) {
201 $perm = _check_request_holds_perm($user->id, $user->home_ou);
202 if($perm) { return $perm; }
205 # is this user allowed to have holds of this type?
206 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
207 return $perm if $perm;
209 #enforce the fact that the login is the one requesting the hold
210 $hold->requestor($user->id);
211 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
213 my $resp = $apputils->simplereq(
215 'open-ils.storage.direct.action.hold_request.create', $hold );
218 return OpenSRF::EX::ERROR ("Error creating hold");
225 # makes sure that a user has permission to place the type of requested hold
226 # returns the Perm exception if not allowed, returns undef if all is well
227 sub _check_holds_perm {
228 my($type, $user_id, $org_id) = @_;
232 if($evt = $apputils->check_perms(
233 $user_id, $org_id, "MR_HOLDS")) {
237 } elsif ($type eq "T") {
238 if($evt = $apputils->check_perms(
239 $user_id, $org_id, "TITLE_HOLDS")) {
243 } elsif($type eq "V") {
244 if($evt = $apputils->check_perms(
245 $user_id, $org_id, "VOLUME_HOLDS")) {
249 } elsif($type eq "C") {
250 if($evt = $apputils->check_perms(
251 $user_id, $org_id, "COPY_HOLDS")) {
259 # tests if the given user is allowed to place holds on another's behalf
260 sub _check_request_holds_perm {
263 if(my $evt = $apputils->check_perms(
264 $user_id, $org_id, "REQUEST_HOLDS")) {
269 __PACKAGE__->register_method(
270 method => "retrieve_holds_by_id",
271 api_name => "open-ils.circ.holds.retrieve_by_id",
273 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
274 different from the user, then the requestor must have VIEW_HOLD permissions.
278 sub retrieve_holds_by_id {
279 my($self, $client, $auth, $hold_id) = @_;
280 my $e = new_editor(authtoken=>$auth);
281 $e->checkauth or return $e->event;
282 $e->allowed('VIEW_HOLD') or return $e->event;
284 my $holds = $e->search_action_hold_request(
286 { id => $hold_id , fulfillment_time => undef },
288 order_by => { ahr => "request_time" },
290 flesh_fields => {ahr => ['notes']}
295 flesh_hold_transits($holds);
296 flesh_hold_notices($holds, $e);
301 __PACKAGE__->register_method(
302 method => "retrieve_holds",
303 api_name => "open-ils.circ.holds.retrieve",
305 Retrieves all the holds, with hold transits attached, for the specified
306 user id. The login session is the requestor and if the requestor is
307 different from the user, then the requestor must have VIEW_HOLD permissions.
310 __PACKAGE__->register_method(
311 method => "retrieve_holds",
313 api_name => "open-ils.circ.holds.id_list.retrieve",
315 Retrieves all the hold ids for the specified
316 user id. The login session is the requestor and if the requestor is
317 different from the user, then the requestor must have VIEW_HOLD permissions.
321 my($self, $client, $auth, $user_id, $options) = @_;
323 my $e = new_editor(authtoken=>$auth);
324 return $e->event unless $e->checkauth;
325 $user_id = $e->requestor->id unless defined $user_id;
328 unless($user_id == $e->requestor->id) {
329 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
330 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
331 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
332 $e, $user_id, $e->requestor->id, 'hold.view');
333 return $e->event unless $allowed;
337 my $holds = $e->search_action_hold_request([
339 fulfillment_time => undef,
340 cancel_time => undef,
342 {order_by => {ahr => "request_time"}}
347 $U->ou_ancestor_setting_value(
348 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
350 unless($cancel_count) {
351 $cancel_age = $U->ou_ancestor_setting_value(
352 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
357 # find at most cancel_count canceled holds
358 my $canceled = $e->search_action_hold_request([
360 fulfillment_time => undef,
361 cancel_time => {'!=' => undef},
363 {order_by => {ahr => "cancel_time desc"}, limit => $cancel_count}
365 push(@$holds, @$canceled);
367 } elsif($cancel_age) {
369 # find all of the canceled holds that were canceled within the configured time frame
370 my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
371 $date = $U->epoch2ISO8601($date->epoch);
373 my $canceled = $e->search_action_hold_request([
375 fulfillment_time => undef,
376 cancel_time => {'>=' => $date},
378 {order_by => {ahr => "cancel_time desc"}}
380 push(@$holds, @$canceled);
383 if( ! $self->api_name =~ /id_list/ ) {
384 for my $hold ( @$holds ) {
386 $e->search_action_hold_transit_copy([
388 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
393 if( $self->api_name =~ /id_list/ ) {
394 return [ map { $_->id } @$holds ];
401 __PACKAGE__->register_method(
402 method => 'user_hold_count',
403 api_name => 'open-ils.circ.hold.user.count');
405 sub user_hold_count {
406 my( $self, $conn, $auth, $userid ) = @_;
407 my $e = new_editor(authtoken=>$auth);
408 return $e->event unless $e->checkauth;
409 my $patron = $e->retrieve_actor_user($userid)
411 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
412 return __user_hold_count($self, $e, $userid);
415 sub __user_hold_count {
416 my( $self, $e, $userid ) = @_;
417 my $holds = $e->search_action_hold_request(
419 fulfillment_time => undef,
420 cancel_time => undef,
425 return scalar(@$holds);
429 __PACKAGE__->register_method(
430 method => "retrieve_holds_by_pickup_lib",
431 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
433 Retrieves all the holds, with hold transits attached, for the specified
437 __PACKAGE__->register_method(
438 method => "retrieve_holds_by_pickup_lib",
439 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
441 Retrieves all the hold ids for the specified
445 sub retrieve_holds_by_pickup_lib {
446 my($self, $client, $login_session, $ou_id) = @_;
448 #FIXME -- put an appropriate permission check here
449 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
450 # $login_session, $user_id, 'VIEW_HOLD' );
451 #return $evt if $evt;
453 my $holds = $apputils->simplereq(
455 "open-ils.cstore.direct.action.hold_request.search.atomic",
457 pickup_lib => $ou_id ,
458 fulfillment_time => undef,
461 { order_by => { ahr => "request_time" } });
464 if( ! $self->api_name =~ /id_list/ ) {
465 flesh_hold_transits($holds);
468 if( $self->api_name =~ /id_list/ ) {
469 return [ map { $_->id } @$holds ];
476 __PACKAGE__->register_method(
477 method => "uncancel_hold",
478 api_name => "open-ils.circ.hold.uncancel"
482 my($self, $client, $auth, $hold_id) = @_;
483 my $e = new_editor(authtoken=>$auth, xact=>1);
484 return $e->event unless $e->checkauth;
486 my $hold = $e->retrieve_action_hold_request($hold_id)
487 or return $e->die_event;
488 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
490 return 0 if $hold->fulfillment_time;
491 return 1 unless $hold->cancel_time;
493 # if configured to reset the request time, also reset the expire time
494 if($U->ou_ancestor_setting_value(
495 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
497 $hold->request_time('now');
498 my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
500 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
501 $hold->expire_time($U->epoch2ISO8601($date->epoch));
505 $hold->clear_cancel_time;
506 $hold->clear_cancel_cause;
507 $hold->clear_cancel_note;
508 $e->update_action_hold_request($hold) or return $e->die_event;
511 $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
517 __PACKAGE__->register_method(
518 method => "cancel_hold",
519 api_name => "open-ils.circ.hold.cancel",
521 Cancels the specified hold. The login session
522 is the requestor and if the requestor is different from the usr field
523 on the hold, the requestor must have CANCEL_HOLDS permissions.
524 the hold may be either the hold object or the hold id
528 my($self, $client, $auth, $holdid, $cause, $note) = @_;
530 my $e = new_editor(authtoken=>$auth, xact=>1);
531 return $e->event unless $e->checkauth;
533 my $hold = $e->retrieve_action_hold_request($holdid)
536 if( $e->requestor->id ne $hold->usr ) {
537 return $e->event unless $e->allowed('CANCEL_HOLDS');
540 return 1 if $hold->cancel_time;
542 # If the hold is captured, reset the copy status
543 if( $hold->capture_time and $hold->current_copy ) {
545 my $copy = $e->retrieve_asset_copy($hold->current_copy)
548 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
549 $logger->info("canceling hold $holdid whose item is on the holds shelf");
550 # $logger->info("setting copy to status 'reshelving' on hold cancel");
551 # $copy->status(OILS_COPY_STATUS_RESHELVING);
552 # $copy->editor($e->requestor->id);
553 # $copy->edit_date('now');
554 # $e->update_asset_copy($copy) or return $e->event;
556 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
559 $logger->warn("! canceling hold [$hid] that is in transit");
560 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
563 my $trans = $e->retrieve_action_transit_copy($transid);
564 # Leave the transit alive, but set the copy status to
565 # reshelving so it will be properly reshelved when it gets back home
567 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
568 $e->update_action_transit_copy($trans) or return $e->die_event;
574 $hold->cancel_time('now');
575 $hold->cancel_cause($cause);
576 $hold->cancel_note($note);
577 $e->update_action_hold_request($hold)
580 delete_hold_copy_maps($self, $e, $hold->id);
586 sub delete_hold_copy_maps {
591 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
593 $editor->delete_action_hold_copy_map($_)
594 or return $editor->event;
600 __PACKAGE__->register_method(
601 method => "update_hold",
602 api_name => "open-ils.circ.hold.update",
604 Updates the specified hold. The login session
605 is the requestor and if the requestor is different from the usr field
606 on the hold, the requestor must have UPDATE_HOLDS permissions.
609 __PACKAGE__->register_method(
610 method => "batch_update_hold",
611 api_name => "open-ils.circ.hold.update.batch",
614 Updates the specified hold. The login session
615 is the requestor and if the requestor is different from the usr field
616 on the hold, the requestor must have UPDATE_HOLDS permissions.
620 my($self, $client, $auth, $hold, $values) = @_;
621 my $e = new_editor(authtoken=>$auth, xact=>1);
622 return $e->die_event unless $e->checkauth;
623 my $resp = update_hold_impl($self, $e, $hold, $values);
624 return $resp if $U->event_code($resp);
629 sub batch_update_hold {
630 my($self, $client, $auth, $hold_list, $values_list) = @_;
631 my $e = new_editor(authtoken=>$auth);
632 return $e->die_event unless $e->checkauth;
634 my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list);
638 for my $idx (0..$count-1) {
640 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
641 $e->xact_commit unless $U->event_code($resp);
642 $client->respond($resp);
649 sub update_hold_impl {
650 my($self, $e, $hold, $values) = @_;
653 $hold = $e->retrieve_action_hold_request($values->{id})
654 or return $e->die_event;
655 $hold->$_($values->{$_}) for keys %$values;
658 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
659 or return $e->die_event;
661 # don't allow the user to be changed
662 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
664 if($hold->usr ne $e->requestor->id) {
665 # if the hold is for a different user, make sure the
666 # requestor has the appropriate permissions
667 my $usr = $e->retrieve_actor_user($hold->usr)
668 or return $e->die_event;
669 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
672 # --------------------------------------------------------------
673 # if the hold is on the holds shelf and the pickup lib changes,
674 # we need to create a new transit
675 # --------------------------------------------------------------
676 if( ($orig_hold->pickup_lib ne $hold->pickup_lib) and (_hold_status($e, $hold) == 4)) {
677 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
678 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
679 my $evt = transit_hold($e, $orig_hold, $hold,
680 $e->retrieve_asset_copy($hold->current_copy));
684 update_hold_if_frozen($self, $e, $hold, $orig_hold);
685 $e->update_action_hold_request($hold) or return $e->die_event;
691 my($e, $orig_hold, $hold, $copy) = @_;
692 my $src = $orig_hold->pickup_lib;
693 my $dest = $hold->pickup_lib;
695 $logger->info("putting hold into transit on pickup_lib update");
697 my $transit = Fieldmapper::action::transit_copy->new;
698 $transit->source($src);
699 $transit->dest($dest);
700 $transit->target_copy($copy->id);
701 $transit->source_send_time('now');
702 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
704 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
705 $copy->editor($e->requestor->id);
706 $copy->edit_date('now');
708 $e->create_action_transit_copy($transit) or return $e->die_event;
709 $e->update_asset_copy($copy) or return $e->die_event;
713 # if the hold is frozen, this method ensures that the hold is not "targeted",
714 # that is, it clears the current_copy and prev_check_time to essentiallly
715 # reset the hold. If it is being activated, it runs the targeter in the background
716 sub update_hold_if_frozen {
717 my($self, $e, $hold, $orig_hold) = @_;
718 return if $hold->capture_time;
720 if($U->is_true($hold->frozen)) {
721 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
722 $hold->clear_current_copy;
723 $hold->clear_prev_check_time;
726 if($U->is_true($orig_hold->frozen)) {
727 $logger->info("Running targeter on activated hold ".$hold->id);
728 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
732 __PACKAGE__->register_method(
733 method => "hold_note_CUD",
734 api_name => "open-ils.circ.hold_request.note.cud");
737 my($self, $conn, $auth, $note) = @_;
739 my $e = new_editor(authtoken => $auth, xact => 1);
740 return $e->die_event unless $e->checkauth;
742 my $hold = $e->retrieve_action_hold_request($note->hold)
743 or return $e->die_event;
745 if($hold->usr ne $e->requestor->id) {
746 my $usr = $e->retrieve_actor_user($hold->usr);
747 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
748 $note->staff('t') if $note->isnew;
752 $e->create_action_hold_request_note($note) or return $e->die_event;
753 } elsif($note->ischanged) {
754 $e->update_action_hold_request_note($note) or return $e->die_event;
755 } elsif($note->isdeleted) {
756 $e->delete_action_hold_request_note($note) or return $e->die_event;
765 __PACKAGE__->register_method(
766 method => "retrieve_hold_status",
767 api_name => "open-ils.circ.hold.status.retrieve",
769 Calculates the current status of the hold.
770 the requestor must have VIEW_HOLD permissions if the hold is for a user
771 other than the requestor.
772 Returns -1 on error (for now)
773 Returns 1 for 'waiting for copy to become available'
774 Returns 2 for 'waiting for copy capture'
775 Returns 3 for 'in transit'
776 Returns 4 for 'arrived'
777 Returns 5 for 'hold-shelf-delay'
780 sub retrieve_hold_status {
781 my($self, $client, $auth, $hold_id) = @_;
783 my $e = new_editor(authtoken => $auth);
784 return $e->event unless $e->checkauth;
785 my $hold = $e->retrieve_action_hold_request($hold_id)
788 if( $e->requestor->id != $hold->usr ) {
789 return $e->event unless $e->allowed('VIEW_HOLD');
792 return _hold_status($e, $hold);
798 return 1 unless $hold->current_copy;
799 return 2 unless $hold->capture_time;
801 my $copy = $hold->current_copy;
802 unless( ref $copy ) {
803 $copy = $e->retrieve_asset_copy($hold->current_copy)
807 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
809 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
811 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
812 return 4 unless $hs_wait_interval;
814 # if a hold_shelf_status_delay interval is defined and start_time plus
815 # the interval is greater than now, consider the hold to be in the virtual
816 # "on its way to the holds shelf" status. Return 5.
818 my $transit = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
819 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
820 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clense_ISO8601($start_time));
821 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
823 return 5 if $end_time > DateTime->now;
832 __PACKAGE__->register_method(
833 method => "retrieve_hold_queue_stats",
834 api_name => "open-ils.circ.hold.queue_stats.retrieve",
837 Returns object with total_holds count, queue_position, potential_copies count, and status code
842 sub retrieve_hold_queue_stats {
843 my($self, $conn, $auth, $hold_id) = @_;
844 my $e = new_editor(authtoken => $auth);
845 return $e->event unless $e->checkauth;
846 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
847 if($e->requestor->id != $hold->usr) {
848 return $e->event unless $e->allowed('VIEW_HOLD');
850 return retrieve_hold_queue_status_impl($e, $hold);
853 sub retrieve_hold_queue_status_impl {
857 # The holds queue is defined as the set of holds that share at
858 # least one potential copy with the context hold
859 my $q_holds = $e->json_query({
862 # fetch request_time since it's in the order_by and we're asking for distinct values
863 ahr => ['request_time']
865 from => {ahcm => 'ahr'},
866 order_by => {ahr => ['request_time']},
871 select => {ahcm => ['target_copy']},
873 where => {hold => $hold->id}
880 for my $h (@$q_holds) {
881 last if $h->{hold} == $hold->id;
885 # total count of potential copies
886 my $num_potentials = $e->json_query({
887 select => {ahcm => [{column => 'id', transform => 'count', alias => 'count'}]},
889 where => {hold => $hold->id}
892 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
893 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
894 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
897 total_holds => scalar(@$q_holds),
898 queue_position => $qpos,
899 potential_copies => $num_potentials,
900 status => _hold_status($e, $hold),
901 estimated_wait => int($estimated_wait)
906 sub fetch_open_hold_by_current_copy {
909 my $hold = $apputils->simplereq(
911 'open-ils.cstore.direct.action.hold_request.search.atomic',
912 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
913 return $hold->[0] if ref($hold);
917 sub fetch_related_holds {
920 return $apputils->simplereq(
922 'open-ils.cstore.direct.action.hold_request.search.atomic',
923 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
927 __PACKAGE__->register_method (
928 method => "hold_pull_list",
929 api_name => "open-ils.circ.hold_pull_list.retrieve",
931 Returns a list of holds that need to be "pulled"
936 __PACKAGE__->register_method (
937 method => "hold_pull_list",
938 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
940 Returns a list of hold ID's that need to be "pulled"
947 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
948 my( $reqr, $evt ) = $U->checkses($authtoken);
951 my $org = $reqr->ws_ou || $reqr->home_ou;
952 # the perm locaiton shouldn't really matter here since holds
953 # will exist all over and VIEW_HOLDS should be universal
954 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
957 if( $self->api_name =~ /id_list/ ) {
958 return $U->storagereq(
959 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
960 $org, $limit, $offset );
962 return $U->storagereq(
963 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
964 $org, $limit, $offset );
968 __PACKAGE__->register_method (
969 method => 'fetch_hold_notify',
970 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
973 Returns a list of hold notification objects based on hold id.
974 @param authtoken The loggin session key
975 @param holdid The id of the hold whose notifications we want to retrieve
976 @return An array of hold notification objects, event on error.
980 sub fetch_hold_notify {
981 my( $self, $conn, $authtoken, $holdid ) = @_;
982 my( $requestor, $evt ) = $U->checkses($authtoken);
985 ($hold, $evt) = $U->fetch_hold($holdid);
987 ($patron, $evt) = $U->fetch_user($hold->usr);
990 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
993 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
994 return $U->cstorereq(
995 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
999 __PACKAGE__->register_method (
1000 method => 'create_hold_notify',
1001 api_name => 'open-ils.circ.hold_notification.create',
1003 Creates a new hold notification object
1004 @param authtoken The login session key
1005 @param notification The hold notification object to create
1006 @return ID of the new object on success, Event on error
1010 sub create_hold_notify {
1011 my( $self, $conn, $auth, $note ) = @_;
1012 my $e = new_editor(authtoken=>$auth, xact=>1);
1013 return $e->die_event unless $e->checkauth;
1015 my $hold = $e->retrieve_action_hold_request($note->hold)
1016 or return $e->die_event;
1017 my $patron = $e->retrieve_actor_user($hold->usr)
1018 or return $e->die_event;
1020 return $e->die_event unless
1021 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1023 $note->notify_staff($e->requestor->id);
1024 $e->create_action_hold_notification($note) or return $e->die_event;
1029 __PACKAGE__->register_method (
1030 method => 'create_hold_note',
1031 api_name => 'open-ils.circ.hold_note.create',
1033 Creates a new hold request note object
1034 @param authtoken The login session key
1035 @param note The hold note object to create
1036 @return ID of the new object on success, Event on error
1040 sub create_hold_note {
1041 my( $self, $conn, $auth, $note ) = @_;
1042 my $e = new_editor(authtoken=>$auth, xact=>1);
1043 return $e->die_event unless $e->checkauth;
1045 my $hold = $e->retrieve_action_hold_request($note->hold)
1046 or return $e->die_event;
1047 my $patron = $e->retrieve_actor_user($hold->usr)
1048 or return $e->die_event;
1050 return $e->die_event unless
1051 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1053 $e->create_action_hold_request_note($note) or return $e->die_event;
1058 __PACKAGE__->register_method(
1059 method => 'reset_hold',
1060 api_name => 'open-ils.circ.hold.reset',
1062 Un-captures and un-targets a hold, essentially returning
1063 it to the state it was in directly after it was placed,
1064 then attempts to re-target the hold
1065 @param authtoken The login session key
1066 @param holdid The id of the hold
1072 my( $self, $conn, $auth, $holdid ) = @_;
1074 my ($hold, $evt) = $U->fetch_hold($holdid);
1075 return $evt if $evt;
1076 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1077 return $evt if $evt;
1078 $evt = _reset_hold($self, $reqr, $hold);
1079 return $evt if $evt;
1084 __PACKAGE__->register_method(
1085 method => 'reset_hold_batch',
1086 api_name => 'open-ils.circ.hold.reset.batch'
1089 sub reset_hold_batch {
1090 my($self, $conn, $auth, $hold_ids) = @_;
1092 my $e = new_editor(authtoken => $auth);
1093 return $e->event unless $e->checkauth;
1095 for my $hold_id ($hold_ids) {
1097 my $hold = $e->retrieve_action_hold_request(
1098 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1099 or return $e->event;
1101 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1102 _reset_hold($self, $e->requestor, $hold);
1110 my ($self, $reqr, $hold) = @_;
1112 my $e = new_editor(xact =>1, requestor => $reqr);
1114 $logger->info("reseting hold ".$hold->id);
1116 my $hid = $hold->id;
1118 if( $hold->capture_time and $hold->current_copy ) {
1120 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1121 or return $e->event;
1123 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1124 $logger->info("setting copy to status 'reshelving' on hold retarget");
1125 $copy->status(OILS_COPY_STATUS_RESHELVING);
1126 $copy->editor($e->requestor->id);
1127 $copy->edit_date('now');
1128 $e->update_asset_copy($copy) or return $e->event;
1130 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1132 # We don't want the copy to remain "in transit"
1133 $copy->status(OILS_COPY_STATUS_RESHELVING);
1134 $logger->warn("! reseting hold [$hid] that is in transit");
1135 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1138 my $trans = $e->retrieve_action_transit_copy($transid);
1140 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1141 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1142 $logger->info("Transit abort completed with result $evt");
1143 return $evt unless "$evt" eq 1;
1149 $hold->clear_capture_time;
1150 $hold->clear_current_copy;
1151 $hold->clear_shelf_time;
1153 $e->update_action_hold_request($hold) or return $e->event;
1157 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1163 __PACKAGE__->register_method(
1164 method => 'fetch_open_title_holds',
1165 api_name => 'open-ils.circ.open_holds.retrieve',
1167 Returns a list ids of un-fulfilled holds for a given title id
1168 @param authtoken The login session key
1169 @param id the id of the item whose holds we want to retrieve
1170 @param type The hold type - M, T, V, C
1174 sub fetch_open_title_holds {
1175 my( $self, $conn, $auth, $id, $type, $org ) = @_;
1176 my $e = new_editor( authtoken => $auth );
1177 return $e->event unless $e->checkauth;
1180 $org ||= $e->requestor->ws_ou;
1182 # return $e->search_action_hold_request(
1183 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1185 # XXX make me return IDs in the future ^--
1186 my $holds = $e->search_action_hold_request(
1189 cancel_time => undef,
1191 fulfillment_time => undef
1195 flesh_hold_transits($holds);
1200 sub flesh_hold_transits {
1202 for my $hold ( @$holds ) {
1204 $apputils->simplereq(
1206 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1207 { hold => $hold->id },
1208 { order_by => { ahtc => 'id desc' }, limit => 1 }
1214 sub flesh_hold_notices {
1215 my( $holds, $e ) = @_;
1216 $e ||= new_editor();
1218 for my $hold (@$holds) {
1219 my $notices = $e->search_action_hold_notification(
1221 { hold => $hold->id },
1222 { order_by => { anh => 'notify_time desc' } },
1227 $hold->notify_count(scalar(@$notices));
1229 my $n = $e->retrieve_action_hold_notification($$notices[0])
1230 or return $e->event;
1231 $hold->notify_time($n->notify_time);
1237 __PACKAGE__->register_method(
1238 method => 'fetch_captured_holds',
1239 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1242 Returns a list of un-fulfilled holds for a given title id
1243 @param authtoken The login session key
1244 @param org The org id of the location in question
1248 __PACKAGE__->register_method(
1249 method => 'fetch_captured_holds',
1250 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1253 Returns a list ids of un-fulfilled holds for a given title id
1254 @param authtoken The login session key
1255 @param org The org id of the location in question
1259 sub fetch_captured_holds {
1260 my( $self, $conn, $auth, $org ) = @_;
1262 my $e = new_editor(authtoken => $auth);
1263 return $e->event unless $e->checkauth;
1264 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1266 $org ||= $e->requestor->ws_ou;
1268 my $hold_ids = $e->json_query(
1270 select => { ahr => ['id'] },
1275 fkey => 'current_copy'
1280 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1282 capture_time => { "!=" => undef },
1283 current_copy => { "!=" => undef },
1284 fulfillment_time => undef,
1286 cancel_time => undef,
1292 for my $hold_id (@$hold_ids) {
1293 if($self->api_name =~ /id_list/) {
1294 $conn->respond($hold_id->{id});
1298 $e->retrieve_action_hold_request([
1302 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1303 order_by => {anh => 'notify_time desc'}
1312 __PACKAGE__->register_method(
1313 method => "check_title_hold",
1314 api_name => "open-ils.circ.title_hold.is_possible",
1316 Determines if a hold were to be placed by a given user,
1317 whether or not said hold would have any potential copies
1319 @param authtoken The login session key
1320 @param params A hash of named params including:
1321 patronid - the id of the hold recipient
1322 titleid (brn) - the id of the title to be held
1323 depth - the hold range depth (defaults to 0)
1326 sub check_title_hold {
1327 my( $self, $client, $authtoken, $params ) = @_;
1329 my %params = %$params;
1330 my $titleid = $params{titleid} ||"";
1331 my $volid = $params{volume_id};
1332 my $copyid = $params{copy_id};
1333 my $mrid = $params{mrid} ||"";
1334 my $depth = $params{depth} || 0;
1335 my $pickup_lib = $params{pickup_lib};
1336 my $hold_type = $params{hold_type} || 'T';
1337 my $selection_ou = $params{selection_ou} || $pickup_lib;
1339 my $e = new_editor(authtoken=>$authtoken);
1340 return $e->event unless $e->checkauth;
1341 my $patron = $e->retrieve_actor_user($params{patronid})
1342 or return $e->event;
1344 if( $e->requestor->id ne $patron->id ) {
1345 return $e->event unless
1346 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1349 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1351 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1352 or return $e->event;
1354 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1355 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1357 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1358 # work up the tree and as soon as we find a potential copy, use that depth
1359 # also, make sure we don't go past the hard boundary if it exists
1361 # our min boundary is the greater of user-specified boundary or hard boundary
1362 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1363 $hard_boundary : $$params{depth};
1365 my $depth = $soft_boundary;
1366 while($depth >= $min_depth) {
1367 $logger->info("performing hold possibility check with soft boundary $depth");
1368 my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1369 return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1372 return {success => 0};
1374 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1375 # there is no soft boundary, enforce the hard boundary if it exists
1376 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1377 my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1379 return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1381 return {success => 0};
1385 # no boundaries defined, fall back to user specifed boundary or no boundary
1386 $logger->info("performing hold possibility check with no boundary");
1387 my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1389 return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1391 return {success => 0};
1396 sub do_possibility_checks {
1397 my($e, $patron, $request_lib, $depth, %params) = @_;
1399 my $titleid = $params{titleid} ||"";
1400 my $volid = $params{volume_id};
1401 my $copyid = $params{copy_id};
1402 my $mrid = $params{mrid} ||"";
1403 my $pickup_lib = $params{pickup_lib};
1404 my $hold_type = $params{hold_type} || 'T';
1405 my $selection_ou = $params{selection_ou} || $pickup_lib;
1412 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1414 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1415 $volume = $e->retrieve_asset_call_number($copy->call_number)
1416 or return $e->event;
1417 $title = $e->retrieve_biblio_record_entry($volume->record)
1418 or return $e->event;
1419 return verify_copy_for_hold(
1420 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1422 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1424 $volume = $e->retrieve_asset_call_number($volid)
1425 or return $e->event;
1426 $title = $e->retrieve_biblio_record_entry($volume->record)
1427 or return $e->event;
1429 return _check_volume_hold_is_possible(
1430 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1432 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1434 return _check_title_hold_is_possible(
1435 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1437 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1439 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1440 my @recs = map { $_->source } @$maps;
1441 for my $rec (@recs) {
1442 my @status = _check_title_hold_is_possible(
1443 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1444 return @status if $status[1];
1451 sub create_ranged_org_filter {
1452 my($e, $selection_ou, $depth) = @_;
1454 # find the orgs from which this hold may be fulfilled,
1455 # based on the selection_ou and depth
1457 my $top_org = $e->search_actor_org_unit([
1458 {parent_ou => undef},
1459 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1462 return () if $depth == $top_org->ou_type->depth;
1464 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1465 %org_filter = (circ_lib => []);
1466 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1468 $logger->info("hold org filter at depth $depth and selection_ou ".
1469 "$selection_ou created list of @{$org_filter{circ_lib}}");
1475 sub _check_title_hold_is_possible {
1476 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1478 my $e = new_editor();
1479 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1481 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1482 my $copies = $e->json_query(
1484 select => { acp => ['id', 'circ_lib'] },
1489 fkey => 'call_number',
1493 filter => { id => $titleid },
1498 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1499 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1503 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1508 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1509 return (0) unless @$copies;
1511 # -----------------------------------------------------------------------
1512 # sort the copies into buckets based on their circ_lib proximity to
1513 # the patron's home_ou.
1514 # -----------------------------------------------------------------------
1516 my $home_org = $patron->home_ou;
1517 my $req_org = $request_lib->id;
1519 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1521 $prox_cache{$home_org} =
1522 $e->search_actor_org_unit_proximity({from_org => $home_org})
1523 unless $prox_cache{$home_org};
1524 my $home_prox = $prox_cache{$home_org};
1527 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1528 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1530 my @keys = sort { $a <=> $b } keys %buckets;
1533 if( $home_org ne $req_org ) {
1534 # -----------------------------------------------------------------------
1535 # shove the copies close to the request_lib into the primary buckets
1536 # directly before the farthest away copies. That way, they are not
1537 # given priority, but they are checked before the farthest copies.
1538 # -----------------------------------------------------------------------
1539 $prox_cache{$req_org} =
1540 $e->search_actor_org_unit_proximity({from_org => $req_org})
1541 unless $prox_cache{$req_org};
1542 my $req_prox = $prox_cache{$req_org};
1546 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1547 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1549 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1550 my $new_key = $highest_key - 0.5; # right before the farthest prox
1551 my @keys2 = sort { $a <=> $b } keys %buckets2;
1552 for my $key (@keys2) {
1553 last if $key >= $highest_key;
1554 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1558 @keys = sort { $a <=> $b } keys %buckets;
1562 for my $key (@keys) {
1563 my @cps = @{$buckets{$key}};
1565 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1567 for my $copyid (@cps) {
1569 next if $seen{$copyid};
1570 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1571 my $copy = $e->retrieve_asset_copy($copyid);
1572 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1574 unless($title) { # grab the title if we don't already have it
1575 my $vol = $e->retrieve_asset_call_number(
1576 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1577 $title = $vol->record;
1580 my @status = verify_copy_for_hold(
1581 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1583 return @status if $status[0];
1591 sub _check_volume_hold_is_possible {
1592 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1593 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1594 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1595 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1596 for my $copy ( @$copies ) {
1597 my @status = verify_copy_for_hold(
1598 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1599 return @status if $status[0];
1606 sub verify_copy_for_hold {
1607 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1608 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1609 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1610 { patron => $patron,
1611 requestor => $requestor,
1614 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1615 pickup_lib => $pickup_lib,
1616 request_lib => $request_lib,
1624 ($copy->circ_lib == $pickup_lib) and
1625 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1632 sub find_nearest_permitted_hold {
1635 my $editor = shift; # CStoreEditor object
1636 my $copy = shift; # copy to target
1637 my $user = shift; # staff
1638 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1639 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1641 my $bc = $copy->barcode;
1643 # find any existing holds that already target this copy
1644 my $old_holds = $editor->search_action_hold_request(
1645 { current_copy => $copy->id,
1646 cancel_time => undef,
1647 capture_time => undef
1651 # hold->type "R" means we need this copy
1652 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1655 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1657 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1658 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1660 # search for what should be the best holds for this copy to fulfill
1661 my $best_holds = $U->storagereq(
1662 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1663 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1665 unless(@$best_holds) {
1667 if( my $hold = $$old_holds[0] ) {
1668 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1672 $logger->info("circulator: no suitable holds found for copy $bc");
1673 return (undef, $evt);
1679 # for each potential hold, we have to run the permit script
1680 # to make sure the hold is actually permitted.
1681 for my $holdid (@$best_holds) {
1682 next unless $holdid;
1683 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1685 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1686 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1687 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1689 # see if this hold is permitted
1690 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1691 { patron_id => $hold->usr,
1694 pickup_lib => $hold->pickup_lib,
1695 request_lib => $rlib,
1706 unless( $best_hold ) { # no "good" permitted holds were found
1707 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1708 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1713 $logger->info("circulator: no suitable holds found for copy $bc");
1714 return (undef, $evt);
1717 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1719 # indicate a permitted hold was found
1720 return $best_hold if $check_only;
1722 # we've found a permitted hold. we need to "grab" the copy
1723 # to prevent re-targeted holds (next part) from re-grabbing the copy
1724 $best_hold->current_copy($copy->id);
1725 $editor->update_action_hold_request($best_hold)
1726 or return (undef, $editor->event);
1731 # re-target any other holds that already target this copy
1732 for my $old_hold (@$old_holds) {
1733 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1734 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1735 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1736 $old_hold->clear_current_copy;
1737 $old_hold->clear_prev_check_time;
1738 $editor->update_action_hold_request($old_hold)
1739 or return (undef, $editor->event);
1740 push(@retarget, $old_hold->id);
1743 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1751 __PACKAGE__->register_method(
1752 method => 'all_rec_holds',
1753 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1757 my( $self, $conn, $auth, $title_id, $args ) = @_;
1759 my $e = new_editor(authtoken=>$auth);
1760 $e->checkauth or return $e->event;
1761 $e->allowed('VIEW_HOLD') or return $e->event;
1764 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
1765 $args->{cancel_time} = undef;
1767 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1769 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1771 $resp->{metarecord_holds} = $e->search_action_hold_request(
1772 { hold_type => OILS_HOLD_TYPE_METARECORD,
1773 target => $mr_map->metarecord,
1779 $resp->{title_holds} = $e->search_action_hold_request(
1781 hold_type => OILS_HOLD_TYPE_TITLE,
1782 target => $title_id,
1786 my $vols = $e->search_asset_call_number(
1787 { record => $title_id, deleted => 'f' }, {idlist=>1});
1789 return $resp unless @$vols;
1791 $resp->{volume_holds} = $e->search_action_hold_request(
1793 hold_type => OILS_HOLD_TYPE_VOLUME,
1798 my $copies = $e->search_asset_copy(
1799 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1801 return $resp unless @$copies;
1803 $resp->{copy_holds} = $e->search_action_hold_request(
1805 hold_type => OILS_HOLD_TYPE_COPY,
1817 __PACKAGE__->register_method(
1818 method => 'uber_hold',
1820 api_name => 'open-ils.circ.hold.details.retrieve'
1824 my($self, $client, $auth, $hold_id) = @_;
1825 my $e = new_editor(authtoken=>$auth);
1826 $e->checkauth or return $e->event;
1827 $e->allowed('VIEW_HOLD') or return $e->event;
1831 my $hold = $e->retrieve_action_hold_request(
1836 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1839 ) or return $e->event;
1841 my $user = $hold->usr;
1842 $hold->usr($user->id);
1844 my $card = $e->retrieve_actor_card($user->card)
1845 or return $e->event;
1847 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1849 flesh_hold_notices([$hold], $e);
1850 flesh_hold_transits([$hold]);
1852 my $details = retrieve_hold_queue_status_impl($e, $hold);
1859 patron_first => $user->first_given_name,
1860 patron_last => $user->family_name,
1861 patron_barcode => $card->barcode,
1868 # -----------------------------------------------------
1869 # Returns the MVR object that represents what the
1871 # -----------------------------------------------------
1873 my( $e, $hold ) = @_;
1879 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1880 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1881 or return $e->event;
1882 $tid = $mr->master_record;
1884 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1885 $tid = $hold->target;
1887 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1888 $volume = $e->retrieve_asset_call_number($hold->target)
1889 or return $e->event;
1890 $tid = $volume->record;
1892 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1893 $copy = $e->retrieve_asset_copy($hold->target)
1894 or return $e->event;
1895 $volume = $e->retrieve_asset_call_number($copy->call_number)
1896 or return $e->event;
1897 $tid = $volume->record;
1900 if(!$copy and ref $hold->current_copy ) {
1901 $copy = $hold->current_copy;
1902 $hold->current_copy($copy->id);
1905 if(!$volume and $copy) {
1906 $volume = $e->retrieve_asset_call_number($copy->call_number);
1909 # TODO return metarcord mvr for M holds
1910 my $title = $e->retrieve_biblio_record_entry($tid);
1911 return ( $U->record_to_mvr($title), $volume, $copy );