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 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
115 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
117 if( $t eq OILS_HOLD_TYPE_METARECORD )
118 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
120 if( $t eq OILS_HOLD_TYPE_TITLE )
121 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
123 if( $t eq OILS_HOLD_TYPE_VOLUME )
124 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
126 if( $t eq OILS_HOLD_TYPE_COPY )
127 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
129 return $pevt if $pevt;
133 for my $evt (@events) {
135 my $name = $evt->{textcode};
136 return $e->event unless $e->allowed("$name.override", $porg);
143 # set the configured expire time
144 unless($hold->expire_time) {
145 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
147 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
148 $hold->expire_time($U->epoch2ISO8601($date->epoch));
152 $hold->requestor($e->requestor->id);
153 $hold->request_lib($e->requestor->ws_ou);
154 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
155 $hold = $e->create_action_hold_request($hold) or return $e->event;
159 $conn->respond_complete($hold->id);
162 'open-ils.storage.action.hold_request.copy_targeter',
163 undef, $hold->id ) unless $U->is_true($hold->frozen);
169 my( $self, $client, $login_session, @holds) = @_;
171 if(!@holds){return 0;}
172 my( $user, $evt ) = $apputils->checkses($login_session);
176 if(ref($holds[0]) eq 'ARRAY') {
178 } else { $holds = [ @holds ]; }
180 $logger->debug("Iterating over holds requests...");
182 for my $hold (@$holds) {
185 my $type = $hold->hold_type;
187 $logger->activity("User " . $user->id .
188 " creating new hold of type $type for user " . $hold->usr);
191 if($user->id ne $hold->usr) {
192 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
202 # am I allowed to place holds for this user?
203 if($hold->requestor ne $hold->usr) {
204 $perm = _check_request_holds_perm($user->id, $user->home_ou);
205 if($perm) { return $perm; }
208 # is this user allowed to have holds of this type?
209 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
210 return $perm if $perm;
212 #enforce the fact that the login is the one requesting the hold
213 $hold->requestor($user->id);
214 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
216 my $resp = $apputils->simplereq(
218 'open-ils.storage.direct.action.hold_request.create', $hold );
221 return OpenSRF::EX::ERROR ("Error creating hold");
228 # makes sure that a user has permission to place the type of requested hold
229 # returns the Perm exception if not allowed, returns undef if all is well
230 sub _check_holds_perm {
231 my($type, $user_id, $org_id) = @_;
235 if($evt = $apputils->check_perms(
236 $user_id, $org_id, "MR_HOLDS")) {
240 } elsif ($type eq "T") {
241 if($evt = $apputils->check_perms(
242 $user_id, $org_id, "TITLE_HOLDS")) {
246 } elsif($type eq "V") {
247 if($evt = $apputils->check_perms(
248 $user_id, $org_id, "VOLUME_HOLDS")) {
252 } elsif($type eq "C") {
253 if($evt = $apputils->check_perms(
254 $user_id, $org_id, "COPY_HOLDS")) {
262 # tests if the given user is allowed to place holds on another's behalf
263 sub _check_request_holds_perm {
266 if(my $evt = $apputils->check_perms(
267 $user_id, $org_id, "REQUEST_HOLDS")) {
272 __PACKAGE__->register_method(
273 method => "retrieve_holds_by_id",
274 api_name => "open-ils.circ.holds.retrieve_by_id",
276 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
277 different from the user, then the requestor must have VIEW_HOLD permissions.
281 sub retrieve_holds_by_id {
282 my($self, $client, $auth, $hold_id) = @_;
283 my $e = new_editor(authtoken=>$auth);
284 $e->checkauth or return $e->event;
285 $e->allowed('VIEW_HOLD') or return $e->event;
287 my $holds = $e->search_action_hold_request(
289 { id => $hold_id , fulfillment_time => undef },
291 order_by => { ahr => "request_time" },
293 flesh_fields => {ahr => ['notes']}
298 flesh_hold_transits($holds);
299 flesh_hold_notices($holds, $e);
304 __PACKAGE__->register_method(
305 method => "retrieve_holds",
306 api_name => "open-ils.circ.holds.retrieve",
308 Retrieves all the holds, with hold transits attached, for the specified
309 user id. The login session is the requestor and if the requestor is
310 different from the user, then the requestor must have VIEW_HOLD permissions.
313 __PACKAGE__->register_method(
314 method => "retrieve_holds",
316 api_name => "open-ils.circ.holds.id_list.retrieve",
318 Retrieves all the hold ids for the specified
319 user id. The login session is the requestor and if the requestor is
320 different from the user, then the requestor must have VIEW_HOLD permissions.
324 __PACKAGE__->register_method(
325 method => "retrieve_holds",
327 api_name => "open-ils.circ.holds.canceled.retrieve",
330 __PACKAGE__->register_method(
331 method => "retrieve_holds",
333 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
338 my($self, $client, $auth, $user_id, $options) = @_;
340 my $e = new_editor(authtoken=>$auth);
341 return $e->event unless $e->checkauth;
342 $user_id = $e->requestor->id unless defined $user_id;
345 unless($user_id == $e->requestor->id) {
346 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
347 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
348 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
349 $e, $user_id, $e->requestor->id, 'hold.view');
350 return $e->event unless $allowed;
356 if($self->api_name !~ /canceled/) {
358 # Fetch the active holds
360 $holds = $e->search_action_hold_request([
362 fulfillment_time => undef,
363 cancel_time => undef,
365 {order_by => {ahr => "request_time"}}
370 # Fetch the canceled holds
374 $U->ou_ancestor_setting_value(
375 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
377 unless($cancel_count) {
378 $cancel_age = $U->ou_ancestor_setting_value(
379 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
382 if($cancel_count) { # limit by count
384 # find at most cancel_count canceled holds
385 $holds = $e->search_action_hold_request([
387 fulfillment_time => undef,
388 cancel_time => {'!=' => undef},
390 {order_by => {ahr => "cancel_time desc"}, limit => $cancel_count}
393 } elsif($cancel_age) { # limit by age
395 # find all of the canceled holds that were canceled within the configured time frame
396 my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
397 $date = $U->epoch2ISO8601($date->epoch);
399 $holds = $e->search_action_hold_request([
401 fulfillment_time => undef,
402 cancel_time => {'>=' => $date},
404 {order_by => {ahr => "cancel_time desc"}}
409 if( ! $self->api_name =~ /id_list/ ) {
410 for my $hold ( @$holds ) {
412 $e->search_action_hold_transit_copy([
414 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
419 if( $self->api_name =~ /id_list/ ) {
420 return [ map { $_->id } @$holds ];
427 __PACKAGE__->register_method(
428 method => 'user_hold_count',
429 api_name => 'open-ils.circ.hold.user.count');
431 sub user_hold_count {
432 my( $self, $conn, $auth, $userid ) = @_;
433 my $e = new_editor(authtoken=>$auth);
434 return $e->event unless $e->checkauth;
435 my $patron = $e->retrieve_actor_user($userid)
437 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
438 return __user_hold_count($self, $e, $userid);
441 sub __user_hold_count {
442 my( $self, $e, $userid ) = @_;
443 my $holds = $e->search_action_hold_request(
445 fulfillment_time => undef,
446 cancel_time => undef,
451 return scalar(@$holds);
455 __PACKAGE__->register_method(
456 method => "retrieve_holds_by_pickup_lib",
457 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
459 Retrieves all the holds, with hold transits attached, for the specified
463 __PACKAGE__->register_method(
464 method => "retrieve_holds_by_pickup_lib",
465 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
467 Retrieves all the hold ids for the specified
471 sub retrieve_holds_by_pickup_lib {
472 my($self, $client, $login_session, $ou_id) = @_;
474 #FIXME -- put an appropriate permission check here
475 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
476 # $login_session, $user_id, 'VIEW_HOLD' );
477 #return $evt if $evt;
479 my $holds = $apputils->simplereq(
481 "open-ils.cstore.direct.action.hold_request.search.atomic",
483 pickup_lib => $ou_id ,
484 fulfillment_time => undef,
487 { order_by => { ahr => "request_time" } });
490 if( ! $self->api_name =~ /id_list/ ) {
491 flesh_hold_transits($holds);
494 if( $self->api_name =~ /id_list/ ) {
495 return [ map { $_->id } @$holds ];
502 __PACKAGE__->register_method(
503 method => "uncancel_hold",
504 api_name => "open-ils.circ.hold.uncancel"
508 my($self, $client, $auth, $hold_id) = @_;
509 my $e = new_editor(authtoken=>$auth, xact=>1);
510 return $e->event unless $e->checkauth;
512 my $hold = $e->retrieve_action_hold_request($hold_id)
513 or return $e->die_event;
514 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
516 return 0 if $hold->fulfillment_time;
517 return 1 unless $hold->cancel_time;
519 # if configured to reset the request time, also reset the expire time
520 if($U->ou_ancestor_setting_value(
521 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
523 $hold->request_time('now');
524 my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
526 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
527 $hold->expire_time($U->epoch2ISO8601($date->epoch));
531 $hold->clear_cancel_time;
532 $hold->clear_cancel_cause;
533 $hold->clear_cancel_note;
534 $e->update_action_hold_request($hold) or return $e->die_event;
537 $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
543 __PACKAGE__->register_method(
544 method => "cancel_hold",
545 api_name => "open-ils.circ.hold.cancel",
547 Cancels the specified hold. The login session
548 is the requestor and if the requestor is different from the usr field
549 on the hold, the requestor must have CANCEL_HOLDS permissions.
550 the hold may be either the hold object or the hold id
554 my($self, $client, $auth, $holdid, $cause, $note) = @_;
556 my $e = new_editor(authtoken=>$auth, xact=>1);
557 return $e->event unless $e->checkauth;
559 my $hold = $e->retrieve_action_hold_request($holdid)
562 if( $e->requestor->id ne $hold->usr ) {
563 return $e->event unless $e->allowed('CANCEL_HOLDS');
566 return 1 if $hold->cancel_time;
568 # If the hold is captured, reset the copy status
569 if( $hold->capture_time and $hold->current_copy ) {
571 my $copy = $e->retrieve_asset_copy($hold->current_copy)
574 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
575 $logger->info("canceling hold $holdid whose item is on the holds shelf");
576 # $logger->info("setting copy to status 'reshelving' on hold cancel");
577 # $copy->status(OILS_COPY_STATUS_RESHELVING);
578 # $copy->editor($e->requestor->id);
579 # $copy->edit_date('now');
580 # $e->update_asset_copy($copy) or return $e->event;
582 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
585 $logger->warn("! canceling hold [$hid] that is in transit");
586 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
589 my $trans = $e->retrieve_action_transit_copy($transid);
590 # Leave the transit alive, but set the copy status to
591 # reshelving so it will be properly reshelved when it gets back home
593 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
594 $e->update_action_transit_copy($trans) or return $e->die_event;
600 $hold->cancel_time('now');
601 $hold->cancel_cause($cause);
602 $hold->cancel_note($note);
603 $e->update_action_hold_request($hold)
606 delete_hold_copy_maps($self, $e, $hold->id);
612 sub delete_hold_copy_maps {
617 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
619 $editor->delete_action_hold_copy_map($_)
620 or return $editor->event;
626 __PACKAGE__->register_method(
627 method => "update_hold",
628 api_name => "open-ils.circ.hold.update",
630 Updates the specified hold. The login session
631 is the requestor and if the requestor is different from the usr field
632 on the hold, the requestor must have UPDATE_HOLDS permissions.
635 __PACKAGE__->register_method(
636 method => "batch_update_hold",
637 api_name => "open-ils.circ.hold.update.batch",
640 Updates the specified hold. The login session
641 is the requestor and if the requestor is different from the usr field
642 on the hold, the requestor must have UPDATE_HOLDS permissions.
646 my($self, $client, $auth, $hold, $values) = @_;
647 my $e = new_editor(authtoken=>$auth, xact=>1);
648 return $e->die_event unless $e->checkauth;
649 my $resp = update_hold_impl($self, $e, $hold, $values);
650 return $resp if $U->event_code($resp);
655 sub batch_update_hold {
656 my($self, $client, $auth, $hold_list, $values_list) = @_;
657 my $e = new_editor(authtoken=>$auth);
658 return $e->die_event unless $e->checkauth;
660 my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list);
664 for my $idx (0..$count-1) {
666 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
667 $e->xact_commit unless $U->event_code($resp);
668 $client->respond($resp);
675 sub update_hold_impl {
676 my($self, $e, $hold, $values) = @_;
679 $hold = $e->retrieve_action_hold_request($values->{id})
680 or return $e->die_event;
681 $hold->$_($values->{$_}) for keys %$values;
684 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
685 or return $e->die_event;
687 # don't allow the user to be changed
688 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
690 if($hold->usr ne $e->requestor->id) {
691 # if the hold is for a different user, make sure the
692 # requestor has the appropriate permissions
693 my $usr = $e->retrieve_actor_user($hold->usr)
694 or return $e->die_event;
695 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
699 # --------------------------------------------------------------
700 # Changing the request time is like playing God
701 # --------------------------------------------------------------
702 if($hold->request_time ne $orig_hold->request_time) {
703 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
704 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
707 # --------------------------------------------------------------
708 # if the hold is on the holds shelf or in transit and the pickup
709 # lib changes we need to create a new transit.
710 # --------------------------------------------------------------
711 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
713 my $status = _hold_status($e, $hold);
715 if($status == 3) { # in transit
717 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
718 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
720 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
722 # update the transit to reflect the new pickup location
723 my $transit = $e->search_action_hold_transit_copy(
724 {hold=>$hold->id, dest_recv_time => undef})->[0]
725 or return $e->die_event;
727 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
728 $transit->dest($hold->pickup_lib);
729 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
731 } elsif($status == 4) { # on holds shelf
733 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
734 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
736 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
738 # create the new transit
739 my $evt = transit_hold($e, $orig_hold, $hold, $e->retrieve_asset_copy($hold->current_copy));
744 update_hold_if_frozen($self, $e, $hold, $orig_hold);
745 $e->update_action_hold_request($hold) or return $e->die_event;
748 # a change to mint-condition changes the set of potential copies, so retarget the hold;
749 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
750 _reset_hold($self, $e->requestor, $hold)
757 my($e, $orig_hold, $hold, $copy) = @_;
758 my $src = $orig_hold->pickup_lib;
759 my $dest = $hold->pickup_lib;
761 $logger->info("putting hold into transit on pickup_lib update");
763 my $transit = Fieldmapper::action::hold_transit_copy->new;
764 $transit->hold($hold->id);
765 $transit->source($src);
766 $transit->dest($dest);
767 $transit->target_copy($copy->id);
768 $transit->source_send_time('now');
769 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
771 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
772 $copy->editor($e->requestor->id);
773 $copy->edit_date('now');
775 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
776 $e->update_asset_copy($copy) or return $e->die_event;
780 # if the hold is frozen, this method ensures that the hold is not "targeted",
781 # that is, it clears the current_copy and prev_check_time to essentiallly
782 # reset the hold. If it is being activated, it runs the targeter in the background
783 sub update_hold_if_frozen {
784 my($self, $e, $hold, $orig_hold) = @_;
785 return if $hold->capture_time;
787 if($U->is_true($hold->frozen)) {
788 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
789 $hold->clear_current_copy;
790 $hold->clear_prev_check_time;
793 if($U->is_true($orig_hold->frozen)) {
794 $logger->info("Running targeter on activated hold ".$hold->id);
795 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
799 __PACKAGE__->register_method(
800 method => "hold_note_CUD",
801 api_name => "open-ils.circ.hold_request.note.cud");
804 my($self, $conn, $auth, $note) = @_;
806 my $e = new_editor(authtoken => $auth, xact => 1);
807 return $e->die_event unless $e->checkauth;
809 my $hold = $e->retrieve_action_hold_request($note->hold)
810 or return $e->die_event;
812 if($hold->usr ne $e->requestor->id) {
813 my $usr = $e->retrieve_actor_user($hold->usr);
814 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
815 $note->staff('t') if $note->isnew;
819 $e->create_action_hold_request_note($note) or return $e->die_event;
820 } elsif($note->ischanged) {
821 $e->update_action_hold_request_note($note) or return $e->die_event;
822 } elsif($note->isdeleted) {
823 $e->delete_action_hold_request_note($note) or return $e->die_event;
832 __PACKAGE__->register_method(
833 method => "retrieve_hold_status",
834 api_name => "open-ils.circ.hold.status.retrieve",
836 Calculates the current status of the hold.
837 the requestor must have VIEW_HOLD permissions if the hold is for a user
838 other than the requestor.
839 Returns -1 on error (for now)
840 Returns 1 for 'waiting for copy to become available'
841 Returns 2 for 'waiting for copy capture'
842 Returns 3 for 'in transit'
843 Returns 4 for 'arrived'
844 Returns 5 for 'hold-shelf-delay'
847 sub retrieve_hold_status {
848 my($self, $client, $auth, $hold_id) = @_;
850 my $e = new_editor(authtoken => $auth);
851 return $e->event unless $e->checkauth;
852 my $hold = $e->retrieve_action_hold_request($hold_id)
855 if( $e->requestor->id != $hold->usr ) {
856 return $e->event unless $e->allowed('VIEW_HOLD');
859 return _hold_status($e, $hold);
865 return 1 unless $hold->current_copy;
866 return 2 unless $hold->capture_time;
868 my $copy = $hold->current_copy;
869 unless( ref $copy ) {
870 $copy = $e->retrieve_asset_copy($hold->current_copy)
874 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
876 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
878 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
879 return 4 unless $hs_wait_interval;
881 # if a hold_shelf_status_delay interval is defined and start_time plus
882 # the interval is greater than now, consider the hold to be in the virtual
883 # "on its way to the holds shelf" status. Return 5.
885 my $transit = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
886 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
887 $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
888 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
890 return 5 if $end_time > DateTime->now;
899 __PACKAGE__->register_method(
900 method => "retrieve_hold_queue_stats",
901 api_name => "open-ils.circ.hold.queue_stats.retrieve",
904 Returns object with total_holds count, queue_position, potential_copies count, and status code
909 sub retrieve_hold_queue_stats {
910 my($self, $conn, $auth, $hold_id) = @_;
911 my $e = new_editor(authtoken => $auth);
912 return $e->event unless $e->checkauth;
913 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
914 if($e->requestor->id != $hold->usr) {
915 return $e->event unless $e->allowed('VIEW_HOLD');
917 return retrieve_hold_queue_status_impl($e, $hold);
920 sub retrieve_hold_queue_status_impl {
924 # The holds queue is defined as the distinct set of holds that share at
925 # least one potential copy with the context hold, plus any holds that
926 # share the same hold type and target. The latter part exists to
927 # accomodate holds that currently have no potential copies
928 my $q_holds = $e->json_query({
930 # fetch request_time since it's in the order_by and we're asking for distinct values
931 select => {ahr => ['id', 'request_time']},
935 ahcm => {type => 'left'} # there may be no copy maps
938 order_by => {ahr => ['request_time']},
946 select => {ahcm => ['target_copy']},
948 where => {hold => $hold->id}
955 hold_type => $hold->hold_type,
956 target => $hold->target
964 for my $h (@$q_holds) {
965 last if $h->{id} == $hold->id;
969 # total count of potential copies
970 my $num_potentials = $e->json_query({
971 select => {ahcm => [{column => 'id', transform => 'count', alias => 'count'}]},
973 where => {hold => $hold->id}
976 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
977 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
978 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
981 total_holds => scalar(@$q_holds),
982 queue_position => $qpos,
983 potential_copies => $num_potentials->{count},
984 status => _hold_status($e, $hold),
985 estimated_wait => int($estimated_wait)
990 sub fetch_open_hold_by_current_copy {
993 my $hold = $apputils->simplereq(
995 'open-ils.cstore.direct.action.hold_request.search.atomic',
996 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
997 return $hold->[0] if ref($hold);
1001 sub fetch_related_holds {
1004 return $apputils->simplereq(
1006 'open-ils.cstore.direct.action.hold_request.search.atomic',
1007 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1011 __PACKAGE__->register_method (
1012 method => "hold_pull_list",
1013 api_name => "open-ils.circ.hold_pull_list.retrieve",
1015 Returns a list of holds that need to be "pulled"
1020 __PACKAGE__->register_method (
1021 method => "hold_pull_list",
1022 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1024 Returns a list of hold ID's that need to be "pulled"
1029 __PACKAGE__->register_method (
1030 method => "hold_pull_list",
1031 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1033 Returns a list of holds that need to be "pulled"
1039 sub hold_pull_list {
1040 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1041 my( $reqr, $evt ) = $U->checkses($authtoken);
1042 return $evt if $evt;
1044 my $org = $reqr->ws_ou || $reqr->home_ou;
1045 # the perm locaiton shouldn't really matter here since holds
1046 # will exist all over and VIEW_HOLDS should be universal
1047 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1048 return $evt if $evt;
1050 if($self->api_name =~ /count/) {
1052 my $count = $U->storagereq(
1053 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1054 $org, $limit, $offset );
1056 $logger->info("Grabbing pull list for org unit $org with $count items");
1059 } elsif( $self->api_name =~ /id_list/ ) {
1060 return $U->storagereq(
1061 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1062 $org, $limit, $offset );
1065 return $U->storagereq(
1066 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1067 $org, $limit, $offset );
1071 __PACKAGE__->register_method (
1072 method => 'fetch_hold_notify',
1073 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1076 Returns a list of hold notification objects based on hold id.
1077 @param authtoken The loggin session key
1078 @param holdid The id of the hold whose notifications we want to retrieve
1079 @return An array of hold notification objects, event on error.
1083 sub fetch_hold_notify {
1084 my( $self, $conn, $authtoken, $holdid ) = @_;
1085 my( $requestor, $evt ) = $U->checkses($authtoken);
1086 return $evt if $evt;
1087 my ($hold, $patron);
1088 ($hold, $evt) = $U->fetch_hold($holdid);
1089 return $evt if $evt;
1090 ($patron, $evt) = $U->fetch_user($hold->usr);
1091 return $evt if $evt;
1093 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1094 return $evt if $evt;
1096 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1097 return $U->cstorereq(
1098 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1102 __PACKAGE__->register_method (
1103 method => 'create_hold_notify',
1104 api_name => 'open-ils.circ.hold_notification.create',
1106 Creates a new hold notification object
1107 @param authtoken The login session key
1108 @param notification The hold notification object to create
1109 @return ID of the new object on success, Event on error
1113 sub create_hold_notify {
1114 my( $self, $conn, $auth, $note ) = @_;
1115 my $e = new_editor(authtoken=>$auth, xact=>1);
1116 return $e->die_event unless $e->checkauth;
1118 my $hold = $e->retrieve_action_hold_request($note->hold)
1119 or return $e->die_event;
1120 my $patron = $e->retrieve_actor_user($hold->usr)
1121 or return $e->die_event;
1123 return $e->die_event unless
1124 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1126 $note->notify_staff($e->requestor->id);
1127 $e->create_action_hold_notification($note) or return $e->die_event;
1132 __PACKAGE__->register_method (
1133 method => 'create_hold_note',
1134 api_name => 'open-ils.circ.hold_note.create',
1136 Creates a new hold request note object
1137 @param authtoken The login session key
1138 @param note The hold note object to create
1139 @return ID of the new object on success, Event on error
1143 sub create_hold_note {
1144 my( $self, $conn, $auth, $note ) = @_;
1145 my $e = new_editor(authtoken=>$auth, xact=>1);
1146 return $e->die_event unless $e->checkauth;
1148 my $hold = $e->retrieve_action_hold_request($note->hold)
1149 or return $e->die_event;
1150 my $patron = $e->retrieve_actor_user($hold->usr)
1151 or return $e->die_event;
1153 return $e->die_event unless
1154 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1156 $e->create_action_hold_request_note($note) or return $e->die_event;
1161 __PACKAGE__->register_method(
1162 method => 'reset_hold',
1163 api_name => 'open-ils.circ.hold.reset',
1165 Un-captures and un-targets a hold, essentially returning
1166 it to the state it was in directly after it was placed,
1167 then attempts to re-target the hold
1168 @param authtoken The login session key
1169 @param holdid The id of the hold
1175 my( $self, $conn, $auth, $holdid ) = @_;
1177 my ($hold, $evt) = $U->fetch_hold($holdid);
1178 return $evt if $evt;
1179 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1180 return $evt if $evt;
1181 $evt = _reset_hold($self, $reqr, $hold);
1182 return $evt if $evt;
1187 __PACKAGE__->register_method(
1188 method => 'reset_hold_batch',
1189 api_name => 'open-ils.circ.hold.reset.batch'
1192 sub reset_hold_batch {
1193 my($self, $conn, $auth, $hold_ids) = @_;
1195 my $e = new_editor(authtoken => $auth);
1196 return $e->event unless $e->checkauth;
1198 for my $hold_id ($hold_ids) {
1200 my $hold = $e->retrieve_action_hold_request(
1201 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1202 or return $e->event;
1204 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1205 _reset_hold($self, $e->requestor, $hold);
1213 my ($self, $reqr, $hold) = @_;
1215 my $e = new_editor(xact =>1, requestor => $reqr);
1217 $logger->info("reseting hold ".$hold->id);
1219 my $hid = $hold->id;
1221 if( $hold->capture_time and $hold->current_copy ) {
1223 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1224 or return $e->event;
1226 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1227 $logger->info("setting copy to status 'reshelving' on hold retarget");
1228 $copy->status(OILS_COPY_STATUS_RESHELVING);
1229 $copy->editor($e->requestor->id);
1230 $copy->edit_date('now');
1231 $e->update_asset_copy($copy) or return $e->event;
1233 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1235 # We don't want the copy to remain "in transit"
1236 $copy->status(OILS_COPY_STATUS_RESHELVING);
1237 $logger->warn("! reseting hold [$hid] that is in transit");
1238 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1241 my $trans = $e->retrieve_action_transit_copy($transid);
1243 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1244 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1245 $logger->info("Transit abort completed with result $evt");
1246 return $evt unless "$evt" eq 1;
1252 $hold->clear_capture_time;
1253 $hold->clear_current_copy;
1254 $hold->clear_shelf_time;
1255 $hold->clear_shelf_expire_time;
1257 $e->update_action_hold_request($hold) or return $e->event;
1261 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1267 __PACKAGE__->register_method(
1268 method => 'fetch_open_title_holds',
1269 api_name => 'open-ils.circ.open_holds.retrieve',
1271 Returns a list ids of un-fulfilled holds for a given title id
1272 @param authtoken The login session key
1273 @param id the id of the item whose holds we want to retrieve
1274 @param type The hold type - M, T, V, C
1278 sub fetch_open_title_holds {
1279 my( $self, $conn, $auth, $id, $type, $org ) = @_;
1280 my $e = new_editor( authtoken => $auth );
1281 return $e->event unless $e->checkauth;
1284 $org ||= $e->requestor->ws_ou;
1286 # return $e->search_action_hold_request(
1287 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1289 # XXX make me return IDs in the future ^--
1290 my $holds = $e->search_action_hold_request(
1293 cancel_time => undef,
1295 fulfillment_time => undef
1299 flesh_hold_transits($holds);
1304 sub flesh_hold_transits {
1306 for my $hold ( @$holds ) {
1308 $apputils->simplereq(
1310 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1311 { hold => $hold->id },
1312 { order_by => { ahtc => 'id desc' }, limit => 1 }
1318 sub flesh_hold_notices {
1319 my( $holds, $e ) = @_;
1320 $e ||= new_editor();
1322 for my $hold (@$holds) {
1323 my $notices = $e->search_action_hold_notification(
1325 { hold => $hold->id },
1326 { order_by => { anh => 'notify_time desc' } },
1331 $hold->notify_count(scalar(@$notices));
1333 my $n = $e->retrieve_action_hold_notification($$notices[0])
1334 or return $e->event;
1335 $hold->notify_time($n->notify_time);
1341 __PACKAGE__->register_method(
1342 method => 'fetch_captured_holds',
1343 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1346 Returns a list of un-fulfilled holds for a given title id
1347 @param authtoken The login session key
1348 @param org The org id of the location in question
1352 __PACKAGE__->register_method(
1353 method => 'fetch_captured_holds',
1354 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1357 Returns a list ids of un-fulfilled holds for a given title id
1358 @param authtoken The login session key
1359 @param org The org id of the location in question
1363 sub fetch_captured_holds {
1364 my( $self, $conn, $auth, $org ) = @_;
1366 my $e = new_editor(authtoken => $auth);
1367 return $e->event unless $e->checkauth;
1368 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1370 $org ||= $e->requestor->ws_ou;
1372 my $hold_ids = $e->json_query(
1374 select => { ahr => ['id'] },
1379 fkey => 'current_copy'
1384 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1386 capture_time => { "!=" => undef },
1387 current_copy => { "!=" => undef },
1388 fulfillment_time => undef,
1390 cancel_time => undef,
1396 for my $hold_id (@$hold_ids) {
1397 if($self->api_name =~ /id_list/) {
1398 $conn->respond($hold_id->{id});
1402 $e->retrieve_action_hold_request([
1406 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1407 order_by => {anh => 'notify_time desc'}
1416 __PACKAGE__->register_method(
1417 method => "check_title_hold",
1418 api_name => "open-ils.circ.title_hold.is_possible",
1420 Determines if a hold were to be placed by a given user,
1421 whether or not said hold would have any potential copies
1423 @param authtoken The login session key
1424 @param params A hash of named params including:
1425 patronid - the id of the hold recipient
1426 titleid (brn) - the id of the title to be held
1427 depth - the hold range depth (defaults to 0)
1430 sub check_title_hold {
1431 my( $self, $client, $authtoken, $params ) = @_;
1433 my %params = %$params;
1434 my $titleid = $params{titleid} ||"";
1435 my $volid = $params{volume_id};
1436 my $copyid = $params{copy_id};
1437 my $mrid = $params{mrid} ||"";
1438 my $depth = $params{depth} || 0;
1439 my $pickup_lib = $params{pickup_lib};
1440 my $hold_type = $params{hold_type} || 'T';
1441 my $selection_ou = $params{selection_ou} || $pickup_lib;
1443 my $e = new_editor(authtoken=>$authtoken);
1444 return $e->event unless $e->checkauth;
1445 my $patron = $e->retrieve_actor_user($params{patronid})
1446 or return $e->event;
1448 if( $e->requestor->id ne $patron->id ) {
1449 return $e->event unless
1450 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1453 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1455 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1456 or return $e->event;
1458 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1459 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1461 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1462 # work up the tree and as soon as we find a potential copy, use that depth
1463 # also, make sure we don't go past the hard boundary if it exists
1465 # our min boundary is the greater of user-specified boundary or hard boundary
1466 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1467 $hard_boundary : $$params{depth};
1469 my $depth = $soft_boundary;
1470 while($depth >= $min_depth) {
1471 $logger->info("performing hold possibility check with soft boundary $depth");
1472 my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1473 return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1476 return {success => 0};
1478 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1479 # there is no soft boundary, enforce the hard boundary if it exists
1480 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1481 my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1483 return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1485 return {success => 0};
1489 # no boundaries defined, fall back to user specifed boundary or no boundary
1490 $logger->info("performing hold possibility check with no boundary");
1491 my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1493 return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1495 return {success => 0};
1500 sub do_possibility_checks {
1501 my($e, $patron, $request_lib, $depth, %params) = @_;
1503 my $titleid = $params{titleid} ||"";
1504 my $volid = $params{volume_id};
1505 my $copyid = $params{copy_id};
1506 my $mrid = $params{mrid} ||"";
1507 my $pickup_lib = $params{pickup_lib};
1508 my $hold_type = $params{hold_type} || 'T';
1509 my $selection_ou = $params{selection_ou} || $pickup_lib;
1516 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1518 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1519 $volume = $e->retrieve_asset_call_number($copy->call_number)
1520 or return $e->event;
1521 $title = $e->retrieve_biblio_record_entry($volume->record)
1522 or return $e->event;
1523 return verify_copy_for_hold(
1524 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1526 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1528 $volume = $e->retrieve_asset_call_number($volid)
1529 or return $e->event;
1530 $title = $e->retrieve_biblio_record_entry($volume->record)
1531 or return $e->event;
1533 return _check_volume_hold_is_possible(
1534 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1536 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1538 return _check_title_hold_is_possible(
1539 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1541 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1543 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1544 my @recs = map { $_->source } @$maps;
1545 for my $rec (@recs) {
1546 my @status = _check_title_hold_is_possible(
1547 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1548 return @status if $status[1];
1555 sub create_ranged_org_filter {
1556 my($e, $selection_ou, $depth) = @_;
1558 # find the orgs from which this hold may be fulfilled,
1559 # based on the selection_ou and depth
1561 my $top_org = $e->search_actor_org_unit([
1562 {parent_ou => undef},
1563 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1566 return () if $depth == $top_org->ou_type->depth;
1568 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1569 %org_filter = (circ_lib => []);
1570 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1572 $logger->info("hold org filter at depth $depth and selection_ou ".
1573 "$selection_ou created list of @{$org_filter{circ_lib}}");
1579 sub _check_title_hold_is_possible {
1580 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1582 my $e = new_editor();
1583 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1585 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1586 my $copies = $e->json_query(
1588 select => { acp => ['id', 'circ_lib'] },
1593 fkey => 'call_number',
1597 filter => { id => $titleid },
1602 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1603 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1607 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1612 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1613 return (0) unless @$copies;
1615 # -----------------------------------------------------------------------
1616 # sort the copies into buckets based on their circ_lib proximity to
1617 # the patron's home_ou.
1618 # -----------------------------------------------------------------------
1620 my $home_org = $patron->home_ou;
1621 my $req_org = $request_lib->id;
1623 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1625 $prox_cache{$home_org} =
1626 $e->search_actor_org_unit_proximity({from_org => $home_org})
1627 unless $prox_cache{$home_org};
1628 my $home_prox = $prox_cache{$home_org};
1631 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1632 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1634 my @keys = sort { $a <=> $b } keys %buckets;
1637 if( $home_org ne $req_org ) {
1638 # -----------------------------------------------------------------------
1639 # shove the copies close to the request_lib into the primary buckets
1640 # directly before the farthest away copies. That way, they are not
1641 # given priority, but they are checked before the farthest copies.
1642 # -----------------------------------------------------------------------
1643 $prox_cache{$req_org} =
1644 $e->search_actor_org_unit_proximity({from_org => $req_org})
1645 unless $prox_cache{$req_org};
1646 my $req_prox = $prox_cache{$req_org};
1650 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1651 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1653 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1654 my $new_key = $highest_key - 0.5; # right before the farthest prox
1655 my @keys2 = sort { $a <=> $b } keys %buckets2;
1656 for my $key (@keys2) {
1657 last if $key >= $highest_key;
1658 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1662 @keys = sort { $a <=> $b } keys %buckets;
1666 for my $key (@keys) {
1667 my @cps = @{$buckets{$key}};
1669 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1671 for my $copyid (@cps) {
1673 next if $seen{$copyid};
1674 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1675 my $copy = $e->retrieve_asset_copy($copyid);
1676 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1678 unless($title) { # grab the title if we don't already have it
1679 my $vol = $e->retrieve_asset_call_number(
1680 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1681 $title = $vol->record;
1684 my @status = verify_copy_for_hold(
1685 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1687 return @status if $status[0];
1695 sub _check_volume_hold_is_possible {
1696 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1697 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1698 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1699 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1700 for my $copy ( @$copies ) {
1701 my @status = verify_copy_for_hold(
1702 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1703 return @status if $status[0];
1710 sub verify_copy_for_hold {
1711 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1712 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1713 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1714 { patron => $patron,
1715 requestor => $requestor,
1718 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1719 pickup_lib => $pickup_lib,
1720 request_lib => $request_lib,
1728 ($copy->circ_lib == $pickup_lib) and
1729 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1736 sub find_nearest_permitted_hold {
1739 my $editor = shift; # CStoreEditor object
1740 my $copy = shift; # copy to target
1741 my $user = shift; # staff
1742 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1743 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1745 my $bc = $copy->barcode;
1747 # find any existing holds that already target this copy
1748 my $old_holds = $editor->search_action_hold_request(
1749 { current_copy => $copy->id,
1750 cancel_time => undef,
1751 capture_time => undef
1755 # hold->type "R" means we need this copy
1756 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1759 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1761 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1762 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1764 # search for what should be the best holds for this copy to fulfill
1765 my $best_holds = $U->storagereq(
1766 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1767 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1769 unless(@$best_holds) {
1771 if( my $hold = $$old_holds[0] ) {
1772 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1776 $logger->info("circulator: no suitable holds found for copy $bc");
1777 return (undef, $evt);
1783 # for each potential hold, we have to run the permit script
1784 # to make sure the hold is actually permitted.
1785 for my $holdid (@$best_holds) {
1786 next unless $holdid;
1787 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1789 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1790 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1791 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1793 # see if this hold is permitted
1794 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1795 { patron_id => $hold->usr,
1798 pickup_lib => $hold->pickup_lib,
1799 request_lib => $rlib,
1810 unless( $best_hold ) { # no "good" permitted holds were found
1811 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1812 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1817 $logger->info("circulator: no suitable holds found for copy $bc");
1818 return (undef, $evt);
1821 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1823 # indicate a permitted hold was found
1824 return $best_hold if $check_only;
1826 # we've found a permitted hold. we need to "grab" the copy
1827 # to prevent re-targeted holds (next part) from re-grabbing the copy
1828 $best_hold->current_copy($copy->id);
1829 $editor->update_action_hold_request($best_hold)
1830 or return (undef, $editor->event);
1835 # re-target any other holds that already target this copy
1836 for my $old_hold (@$old_holds) {
1837 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1838 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1839 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1840 $old_hold->clear_current_copy;
1841 $old_hold->clear_prev_check_time;
1842 $editor->update_action_hold_request($old_hold)
1843 or return (undef, $editor->event);
1844 push(@retarget, $old_hold->id);
1847 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1855 __PACKAGE__->register_method(
1856 method => 'all_rec_holds',
1857 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1861 my( $self, $conn, $auth, $title_id, $args ) = @_;
1863 my $e = new_editor(authtoken=>$auth);
1864 $e->checkauth or return $e->event;
1865 $e->allowed('VIEW_HOLD') or return $e->event;
1868 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
1869 $args->{cancel_time} = undef;
1871 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1873 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1875 $resp->{metarecord_holds} = $e->search_action_hold_request(
1876 { hold_type => OILS_HOLD_TYPE_METARECORD,
1877 target => $mr_map->metarecord,
1883 $resp->{title_holds} = $e->search_action_hold_request(
1885 hold_type => OILS_HOLD_TYPE_TITLE,
1886 target => $title_id,
1890 my $vols = $e->search_asset_call_number(
1891 { record => $title_id, deleted => 'f' }, {idlist=>1});
1893 return $resp unless @$vols;
1895 $resp->{volume_holds} = $e->search_action_hold_request(
1897 hold_type => OILS_HOLD_TYPE_VOLUME,
1902 my $copies = $e->search_asset_copy(
1903 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1905 return $resp unless @$copies;
1907 $resp->{copy_holds} = $e->search_action_hold_request(
1909 hold_type => OILS_HOLD_TYPE_COPY,
1921 __PACKAGE__->register_method(
1922 method => 'uber_hold',
1924 api_name => 'open-ils.circ.hold.details.retrieve'
1928 my($self, $client, $auth, $hold_id) = @_;
1929 my $e = new_editor(authtoken=>$auth);
1930 $e->checkauth or return $e->event;
1931 return uber_hold_impl($e, $hold_id);
1934 __PACKAGE__->register_method(
1935 method => 'batch_uber_hold',
1938 api_name => 'open-ils.circ.hold.details.batch.retrieve'
1941 sub batch_uber_hold {
1942 my($self, $client, $auth, $hold_ids) = @_;
1943 my $e = new_editor(authtoken=>$auth);
1944 $e->checkauth or return $e->event;
1945 $client->respond(uber_hold_impl($e, $_)) for @$hold_ids;
1949 sub uber_hold_impl {
1950 my($e, $hold_id) = @_;
1954 my $hold = $e->retrieve_action_hold_request(
1959 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1962 ) or return $e->event;
1964 if($hold->usr->id ne $e->requestor->id) {
1965 # A user is allowed to see his/her own holds
1966 $e->allowed('VIEW_HOLD') or return $e->event;
1969 my $user = $hold->usr;
1970 $hold->usr($user->id);
1972 my $card = $e->retrieve_actor_card($user->card)
1973 or return $e->event;
1975 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1977 flesh_hold_notices([$hold], $e);
1978 flesh_hold_transits([$hold]);
1980 my $details = retrieve_hold_queue_status_impl($e, $hold);
1987 patron_first => $user->first_given_name,
1988 patron_last => $user->family_name,
1989 patron_barcode => $card->barcode,
1996 # -----------------------------------------------------
1997 # Returns the MVR object that represents what the
1999 # -----------------------------------------------------
2001 my( $e, $hold ) = @_;
2007 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2008 my $mr = $e->retrieve_metabib_metarecord($hold->target)
2009 or return $e->event;
2010 $tid = $mr->master_record;
2012 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
2013 $tid = $hold->target;
2015 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2016 $volume = $e->retrieve_asset_call_number($hold->target)
2017 or return $e->event;
2018 $tid = $volume->record;
2020 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
2021 $copy = $e->retrieve_asset_copy($hold->target)
2022 or return $e->event;
2023 $volume = $e->retrieve_asset_call_number($copy->call_number)
2024 or return $e->event;
2025 $tid = $volume->record;
2028 if(!$copy and ref $hold->current_copy ) {
2029 $copy = $hold->current_copy;
2030 $hold->current_copy($copy->id);
2033 if(!$volume and $copy) {
2034 $volume = $e->retrieve_asset_call_number($copy->call_number);
2037 # TODO return metarcord mvr for M holds
2038 my $title = $e->retrieve_biblio_record_entry($tid);
2039 return ( $U->record_to_mvr($title), $volume, $copy );
2043 __PACKAGE__->register_method(
2044 method => 'clear_shelf_process',
2046 api_name => 'open-ils.circ.hold.clear_shelf.process',
2049 1. Find all holds that have expired on the holds shelf
2051 3. If a clear-shelf status is configured, put targeted copies into this status
2052 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
2053 that are needed for holds. No subsequent action is taken on the holds
2054 or items after grouping.
2059 sub clear_shelf_process {
2060 my($self, $client, $auth, $org_id) = @_;
2062 my $e = new_editor(authtoken=>$auth, xact => 1);
2063 $e->checkauth or return $e->die_event;
2065 $org_id ||= $e->requestor->ws_ou;
2066 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
2068 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
2070 # Find holds on the shelf that have been there too long
2071 my $hold_ids = $e->search_action_hold_request(
2072 { shelf_expire_time => {'<' => 'now'},
2073 pickup_lib => $org_id,
2074 cancel_time => undef,
2075 fulfillment_time => undef,
2076 shelf_time => {'!=' => undef}
2083 for my $hold_id (@$hold_ids) {
2085 $logger->info("Clear shelf processing hold $hold_id");
2087 my $hold = $e->retrieve_action_hold_request([
2090 flesh_fields => {ahr => ['current_copy']}
2094 $hold->cancel_time('now');
2095 $hold->cancel_cause(2); # Hold Shelf expiration
2096 $e->update_action_hold_request($hold) or return $e->die_event;
2098 my $copy = $hold->current_copy;
2101 # if a clear-shelf copy status is defined, update the copy
2102 $copy->status($copy_status);
2103 $copy->edit_date('now');
2104 $copy->editor($e->requestor->id);
2105 $e->update_asset_copy($copy) or return $e->die_event;
2108 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
2112 # copy is needed for a hold
2113 $client->respond({action => 'hold', copy => $copy, hold_id => $hold->id});
2115 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
2117 # copy needs to transit
2118 $client->respond({action => 'transit', copy => $copy, hold_id => $hold->id});
2122 # copy needs to go back to the shelf
2123 $client->respond({action => 'shelf', copy => $copy, hold_id => $hold->id});
2126 push(@holds, $hold);
2131 # tell the client we're done
2132 $client->resopnd_complete;
2134 # fire off the hold cancelation trigger
2135 my $trigger = OpenSRF::AppSession->connect('open-ils.trigger');
2137 for my $hold (@holds) {
2139 my $req = $trigger->request(
2140 'open-ils.trigger.event.autocreate',
2141 'hold_request.cancel.expire_holds_shelf',
2144 # wait for response so don't flood the service
2148 $trigger->disconnect;
2152 __PACKAGE__->register_method(
2153 method => 'usr_hold_summary',
2154 api_name => 'open-ils.circ.holds.user_summary',
2156 Returns a summary of holds statuses for a given user
2160 sub usr_hold_summary {
2161 my($self, $conn, $auth, $user_id) = @_;
2163 my $e = new_editor(authtoken=>$auth);
2164 $e->checkauth or return $e->event;
2165 $e->allowed('VIEW_HOLD') or return $e->event;
2167 my $holds = $e->search_action_hold_request(
2170 fulfillment_time => undef,
2171 cancel_time => undef,
2175 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
2176 $summary{_hold_status($e, $_)} += 1 for @$holds;
2182 __PACKAGE__->register_method(
2183 method => 'hold_has_copy_at',
2184 api_name => 'open-ils.circ.hold.has_copy_at',
2186 Returns the ID of the found copy and name of the shelving location if there is
2187 an available copy at the specified org unit. Returns empty hash otherwise.
2191 sub hold_has_copy_at {
2192 my($self, $conn, $auth, $args) = @_;
2194 my $e = new_editor(authtoken=>$auth);
2195 $e->checkauth or return $e->event;
2197 my $hold_type = $$args{hold_type};
2198 my $hold_target = $$args{hold_target};
2199 my $org_unit = $$args{org_unit};
2202 select => {acp => ['id'], acpl => ['name']},
2205 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
2206 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status'}
2209 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
2213 if($hold_type eq 'C') {
2215 $query->{where}->{'+acp'}->{id} = $hold_target;
2217 } elsif($hold_type eq 'V') {
2219 $query->{where}->{'+acp'}->{call_number} = $hold_target;
2221 } elsif($hold_type eq 'T') {
2223 $query->{from}->{acp}->{acn} = {
2225 fkey => 'call_number',
2229 filter => {id => $hold_target},
2237 $query->{from}->{acp}->{acn} = {
2239 fkey => 'call_number',
2248 filter => {metarecord => $hold_target},
2256 my $res = $e->json_query($query)->[0] or return {};
2257 return {copy => $res->{id}, location => $res->{name}} if $res;
2261 # returns true if the user already has an item checked out
2262 # that could be used to fulfill the requested hold.
2263 sub hold_item_is_checked_out {
2264 my($e, $user_id, $hold_type, $hold_target) = @_;
2267 select => {acp => ['id']},
2268 from => {acp => {}},
2272 in => { # copies for circs the user has checked out
2273 select => {circ => ['target_copy']},
2277 checkin_time => undef,
2279 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2280 {stop_fines => undef}
2290 if($hold_type eq 'C') {
2292 $query->{where}->{'+acp'}->{id} = $hold_target;
2294 } elsif($hold_type eq 'V') {
2296 $query->{where}->{'+acp'}->{call_number} = $hold_target;
2298 } elsif($hold_type eq 'T') {
2300 $query->{from}->{acp}->{acn} = {
2302 fkey => 'call_number',
2306 filter => {id => $hold_target},
2314 $query->{from}->{acp}->{acn} = {
2316 fkey => 'call_number',
2325 filter => {metarecord => $hold_target},
2333 return $e->json_query($query)->[0];
2336 __PACKAGE__->register_method(
2337 method => 'change_hold_title',
2338 api_name => 'open-ils.circ.hold.change_title',
2341 Updates all title level holds targeting the specified bibs to point a new bib./,
2343 {desc => 'Authentication Token', type => 'string'},
2344 {desc => 'New Target Bib Id', type => 'number'},
2345 {desc => 'Old Target Bib Ids', type => 'array'},
2347 return => { desc => '1 on success' }
2351 sub change_hold_title {
2352 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
2354 my $e = new_editor(authtoken=>$auth, xact=>1);
2355 return $e->event unless $e->checkauth;
2357 my $holds = $e->json_query({
2358 "select"=>{"ahr"=>["id"]},
2361 cancel_time => undef,
2362 fulfillment_time => undef,
2368 for my $hold_id (@$holds) {
2369 my $hold = $e->retrieve_action_hold_request([$hold_id->{id}, {
2371 flesh_fields=>{ahr=>['usr']}
2374 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->event;
2375 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
2376 $hold->target( $new_bib_id );
2377 unless ($e->update_action_hold_request($hold)) {
2378 my $evt = $e->event;
2379 $logger->error("Error updating hold " . $evt->textcode . ":" . $evt->desc . ":" . $evt->stacktrace);