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/OpenSRF::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
22 use OpenSRF::EX qw(:try);
25 use OpenSRF::Utils::Logger qw(:logger);
26 use OpenILS::Utils::CStoreEditor q/:funcs/;
27 use OpenILS::Utils::PermitHold;
28 use OpenSRF::Utils::SettingsClient;
29 use OpenILS::Const qw/:const/;
30 use OpenILS::Application::Circ::Transit;
32 my $apputils = "OpenILS::Application::AppUtils";
37 __PACKAGE__->register_method(
38 method => "create_hold",
39 api_name => "open-ils.circ.holds.create",
41 Create a new hold for an item. From a permissions perspective,
42 the login session is used as the 'requestor' of the hold.
43 The hold recipient is determined by the 'usr' setting within
46 First we verify the requestion has holds request permissions.
47 Then we verify that the recipient is allowed to make the given hold.
48 If not, we see if the requestor has "override" capabilities. If not,
49 a permission exception is returned. If permissions allow, we cycle
50 through the set of holds objects and create.
52 If the recipient does not have permission to place multiple holds
53 on a single title and said operation is attempted, a permission
58 __PACKAGE__->register_method(
59 method => "create_hold",
60 api_name => "open-ils.circ.holds.create.override",
62 If the recipient is not allowed to receive the requested hold,
63 call this method to attempt the override
64 @see open-ils.circ.holds.create
69 my( $self, $conn, $auth, @holds ) = @_;
70 my $e = new_editor(authtoken=>$auth, xact=>1);
71 return $e->event unless $e->checkauth;
73 my $override = 1 if $self->api_name =~ /override/;
75 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
79 for my $hold (@$holds) {
84 my $requestor = $e->requestor;
85 my $recipient = $requestor;
88 if( $requestor->id ne $hold->usr ) {
89 # Make sure the requestor is allowed to place holds for
90 # the recipient if they are not the same people
91 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
92 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
95 # Now make sure the recipient is allowed to receive the specified hold
97 my $porg = $recipient->home_ou;
98 my $rid = $e->requestor->id;
99 my $t = $hold->hold_type;
101 # See if a duplicate hold already exists
103 usr => $recipient->id,
105 fulfillment_time => undef,
106 target => $hold->target,
107 cancel_time => undef,
110 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
112 my $existing = $e->search_action_hold_request($sargs);
113 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
115 if( $t eq OILS_HOLD_TYPE_METARECORD )
116 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'MR_HOLDS'); }
118 if( $t eq OILS_HOLD_TYPE_TITLE )
119 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS'); }
121 if( $t eq OILS_HOLD_TYPE_VOLUME )
122 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
124 if( $t eq OILS_HOLD_TYPE_COPY )
125 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
127 return $pevt if $pevt;
131 for my $evt (@events) {
133 my $name = $evt->{textcode};
134 return $e->event unless $e->allowed("$name.override", $porg);
141 $hold->requestor($e->requestor->id);
142 $hold->request_lib($e->requestor->ws_ou);
143 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
144 $hold = $e->create_action_hold_request($hold) or return $e->event;
145 # push( @copyholds, $hold ) if $hold->hold_type eq OILS_HOLD_TYPE_COPY;
150 $conn->respond_complete(1);
152 # Go ahead and target the copy-level holds
154 'open-ils.storage.action.hold_request.copy_targeter',
155 undef, $_->id ) for @holds;
161 my( $self, $client, $login_session, @holds) = @_;
163 if(!@holds){return 0;}
164 my( $user, $evt ) = $apputils->checkses($login_session);
168 if(ref($holds[0]) eq 'ARRAY') {
170 } else { $holds = [ @holds ]; }
172 $logger->debug("Iterating over holds requests...");
174 for my $hold (@$holds) {
177 my $type = $hold->hold_type;
179 $logger->activity("User " . $user->id .
180 " creating new hold of type $type for user " . $hold->usr);
183 if($user->id ne $hold->usr) {
184 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
194 # am I allowed to place holds for this user?
195 if($hold->requestor ne $hold->usr) {
196 $perm = _check_request_holds_perm($user->id, $user->home_ou);
197 if($perm) { return $perm; }
200 # is this user allowed to have holds of this type?
201 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
203 #if there is a requestor, see if the requestor has override privelages
204 if($hold->requestor ne $hold->usr) {
205 $perm = _check_request_holds_override($user->id, $user->home_ou);
206 if($perm) {return $perm;}
213 #enforce the fact that the login is the one requesting the hold
214 $hold->requestor($user->id);
215 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
217 my $resp = $apputils->simplereq(
219 'open-ils.storage.direct.action.hold_request.create', $hold );
222 return OpenSRF::EX::ERROR ("Error creating hold");
229 # makes sure that a user has permission to place the type of requested hold
230 # returns the Perm exception if not allowed, returns undef if all is well
231 sub _check_holds_perm {
232 my($type, $user_id, $org_id) = @_;
236 if($evt = $apputils->check_perms(
237 $user_id, $org_id, "MR_HOLDS")) {
241 } elsif ($type eq "T") {
242 if($evt = $apputils->check_perms(
243 $user_id, $org_id, "TITLE_HOLDS")) {
247 } elsif($type eq "V") {
248 if($evt = $apputils->check_perms(
249 $user_id, $org_id, "VOLUME_HOLDS")) {
253 } elsif($type eq "C") {
254 if($evt = $apputils->check_perms(
255 $user_id, $org_id, "COPY_HOLDS")) {
263 # tests if the given user is allowed to place holds on another's behalf
264 sub _check_request_holds_perm {
267 if(my $evt = $apputils->check_perms(
268 $user_id, $org_id, "REQUEST_HOLDS")) {
273 sub _check_request_holds_override {
276 if(my $evt = $apputils->check_perms(
277 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
282 __PACKAGE__->register_method(
283 method => "retrieve_holds_by_id",
284 api_name => "open-ils.circ.holds.retrieve_by_id",
286 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
287 different from the user, then the requestor must have VIEW_HOLD permissions.
291 sub retrieve_holds_by_id {
292 my($self, $client, $auth, $hold_id) = @_;
293 my $e = new_editor(authtoken=>$auth);
294 $e->checkauth or return $e->event;
295 $e->allowed('VIEW_HOLD') or return $e->event;
297 my $holds = $e->search_action_hold_request(
299 { id => $hold_id , fulfillment_time => undef },
300 { order_by => { ahr => "request_time" } }
304 flesh_hold_transits($holds);
305 flesh_hold_notices($holds, $e);
310 __PACKAGE__->register_method(
311 method => "retrieve_holds",
312 api_name => "open-ils.circ.holds.retrieve",
314 Retrieves all the holds, with hold transits attached, for the specified
315 user id. The login session is the requestor and if the requestor is
316 different from the user, then the requestor must have VIEW_HOLD permissions.
319 __PACKAGE__->register_method(
320 method => "retrieve_holds",
321 api_name => "open-ils.circ.holds.id_list.retrieve",
323 Retrieves all the hold ids for the specified
324 user id. The login session is the requestor and if the requestor is
325 different from the user, then the requestor must have VIEW_HOLD permissions.
329 my($self, $client, $login_session, $user_id) = @_;
331 my( $user, $target, $evt ) = $apputils->checkses_requestor(
332 $login_session, $user_id, 'VIEW_HOLD' );
335 my $holds = $apputils->simplereq(
337 "open-ils.cstore.direct.action.hold_request.search.atomic",
340 fulfillment_time => undef,
341 cancel_time => undef,
343 { order_by => { ahr => "request_time" } }
346 if( ! $self->api_name =~ /id_list/ ) {
347 for my $hold ( @$holds ) {
349 $apputils->simplereq(
351 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
352 { hold => $hold->id },
353 { order_by => { ahtc => 'id desc' }, limit => 1 }
359 if( $self->api_name =~ /id_list/ ) {
360 return [ map { $_->id } @$holds ];
367 __PACKAGE__->register_method(
368 method => 'user_hold_count',
369 api_name => 'open-ils.circ.hold.user.count');
371 sub user_hold_count {
372 my( $self, $conn, $auth, $userid ) = @_;
373 my $e = new_editor(authtoken=>$auth);
374 return $e->event unless $e->checkauth;
375 my $patron = $e->retrieve_actor_user($userid)
377 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
378 return $self->__user_hold_count($e, $userid);
381 sub __user_hold_count {
382 my( $self, $e, $userid ) = @_;
383 my $holds = $e->search_action_hold_request(
385 fulfillment_time => undef,
386 cancel_time => undef,
391 return scalar(@$holds);
395 __PACKAGE__->register_method(
396 method => "retrieve_holds_by_pickup_lib",
397 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
399 Retrieves all the holds, with hold transits attached, for the specified
403 __PACKAGE__->register_method(
404 method => "retrieve_holds_by_pickup_lib",
405 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
407 Retrieves all the hold ids for the specified
411 sub retrieve_holds_by_pickup_lib {
412 my($self, $client, $login_session, $ou_id) = @_;
414 #FIXME -- put an appropriate permission check here
415 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
416 # $login_session, $user_id, 'VIEW_HOLD' );
417 #return $evt if $evt;
419 my $holds = $apputils->simplereq(
421 "open-ils.cstore.direct.action.hold_request.search.atomic",
423 pickup_lib => $ou_id ,
424 fulfillment_time => undef,
427 { order_by => { ahr => "request_time" } });
430 if( ! $self->api_name =~ /id_list/ ) {
431 flesh_hold_transits($holds);
434 if( $self->api_name =~ /id_list/ ) {
435 return [ map { $_->id } @$holds ];
441 __PACKAGE__->register_method(
442 method => "cancel_hold",
443 api_name => "open-ils.circ.hold.cancel",
445 Cancels the specified hold. The login session
446 is the requestor and if the requestor is different from the usr field
447 on the hold, the requestor must have CANCEL_HOLDS permissions.
448 the hold may be either the hold object or the hold id
452 my($self, $client, $auth, $holdid) = @_;
454 my $e = new_editor(authtoken=>$auth, xact=>1);
455 return $e->event unless $e->checkauth;
457 my $hold = $e->retrieve_action_hold_request($holdid)
460 if( $e->requestor->id ne $hold->usr ) {
461 return $e->event unless $e->allowed('CANCEL_HOLDS');
464 return 1 if $hold->cancel_time;
466 # If the hold is captured, reset the copy status
467 if( $hold->capture_time and $hold->current_copy ) {
469 my $copy = $e->retrieve_asset_copy($hold->current_copy)
472 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
473 $logger->info("canceling hold $holdid whose item is on the holds shelf");
474 # $logger->info("setting copy to status 'reshelving' on hold cancel");
475 # $copy->status(OILS_COPY_STATUS_RESHELVING);
476 # $copy->editor($e->requestor->id);
477 # $copy->edit_date('now');
478 # $e->update_asset_copy($copy) or return $e->event;
480 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
483 $logger->warn("! canceling hold [$hid] that is in transit");
484 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
487 my $trans = $e->retrieve_action_transit_copy($transid);
488 # Leave the transit alive, but set the copy status to
489 # reshelving so it will be properly reshelved when it gets back home
491 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
492 $e->update_action_transit_copy($trans) or return $e->die_event;
498 $hold->cancel_time('now');
499 $e->update_action_hold_request($hold)
502 $self->delete_hold_copy_maps($e, $hold->id);
508 sub delete_hold_copy_maps {
513 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
515 $editor->delete_action_hold_copy_map($_)
516 or return $editor->event;
522 __PACKAGE__->register_method(
523 method => "update_hold",
524 api_name => "open-ils.circ.hold.update",
526 Updates the specified hold. The login session
527 is the requestor and if the requestor is different from the usr field
528 on the hold, the requestor must have UPDATE_HOLDS permissions.
532 my($self, $client, $login_session, $hold) = @_;
534 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
535 $login_session, $hold->usr, 'UPDATE_HOLD' );
538 $logger->activity('User ' . $requestor->id .
539 ' updating hold ' . $hold->id . ' for user ' . $target->id );
541 return $U->storagereq(
542 "open-ils.storage.direct.action.hold_request.update", $hold );
546 __PACKAGE__->register_method(
547 method => "retrieve_hold_status",
548 api_name => "open-ils.circ.hold.status.retrieve",
550 Calculates the current status of the hold.
551 the requestor must have VIEW_HOLD permissions if the hold is for a user
552 other than the requestor.
553 Returns -1 on error (for now)
554 Returns 1 for 'waiting for copy to become available'
555 Returns 2 for 'waiting for copy capture'
556 Returns 3 for 'in transit'
557 Returns 4 for 'arrived'
560 sub retrieve_hold_status {
561 my($self, $client, $auth, $hold_id) = @_;
563 my $e = new_editor(authtoken => $auth);
564 return $e->event unless $e->checkauth;
565 my $hold = $e->retrieve_action_hold_request($hold_id)
568 if( $e->requestor->id != $hold->usr ) {
569 return $e->event unless $e->allowed('VIEW_HOLD');
572 return _hold_status($e, $hold);
578 return 1 unless $hold->current_copy;
579 return 2 unless $hold->capture_time;
581 my $copy = $hold->current_copy;
582 unless( ref $copy ) {
583 $copy = $e->retrieve_asset_copy($hold->current_copy)
587 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
588 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
594 #sub find_local_hold {
595 # my( $class, $session, $copy, $user ) = @_;
596 # return $class->find_nearest_permitted_hold($session, $copy, $user);
600 sub fetch_open_hold_by_current_copy {
603 my $hold = $apputils->simplereq(
605 'open-ils.cstore.direct.action.hold_request.search.atomic',
606 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
607 return $hold->[0] if ref($hold);
611 sub fetch_related_holds {
614 return $apputils->simplereq(
616 'open-ils.cstore.direct.action.hold_request.search.atomic',
617 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
621 __PACKAGE__->register_method (
622 method => "hold_pull_list",
623 api_name => "open-ils.circ.hold_pull_list.retrieve",
625 Returns a list of holds that need to be "pulled"
630 __PACKAGE__->register_method (
631 method => "hold_pull_list",
632 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
634 Returns a list of hold ID's that need to be "pulled"
641 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
642 my( $reqr, $evt ) = $U->checkses($authtoken);
645 my $org = $reqr->ws_ou || $reqr->home_ou;
646 # the perm locaiton shouldn't really matter here since holds
647 # will exist all over and VIEW_HOLDS should be universal
648 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
651 if( $self->api_name =~ /id_list/ ) {
652 return $U->storagereq(
653 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
654 $org, $limit, $offset );
656 return $U->storagereq(
657 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
658 $org, $limit, $offset );
662 __PACKAGE__->register_method (
663 method => 'fetch_hold_notify',
664 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
666 Returns a list of hold notification objects based on hold id.
667 @param authtoken The loggin session key
668 @param holdid The id of the hold whose notifications we want to retrieve
669 @return An array of hold notification objects, event on error.
673 sub fetch_hold_notify {
674 my( $self, $conn, $authtoken, $holdid ) = @_;
675 my( $requestor, $evt ) = $U->checkses($authtoken);
678 ($hold, $evt) = $U->fetch_hold($holdid);
680 ($patron, $evt) = $U->fetch_user($hold->usr);
683 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
686 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
687 return $U->cstorereq(
688 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
692 __PACKAGE__->register_method (
693 method => 'create_hold_notify',
694 api_name => 'open-ils.circ.hold_notification.create',
696 Creates a new hold notification object
697 @param authtoken The login session key
698 @param notification The hold notification object to create
699 @return ID of the new object on success, Event on error
703 sub __create_hold_notify {
704 my( $self, $conn, $authtoken, $notification ) = @_;
705 my( $requestor, $evt ) = $U->checkses($authtoken);
708 ($hold, $evt) = $U->fetch_hold($notification->hold);
710 ($patron, $evt) = $U->fetch_user($hold->usr);
713 # XXX perm depth probably doesn't matter here -- should always be consortium level
714 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
717 # Set the proper notifier
718 $notification->notify_staff($requestor->id);
719 my $id = $U->storagereq(
720 'open-ils.storage.direct.action.hold_notification.create', $notification );
721 return $U->DB_UPDATE_FAILED($notification) unless $id;
722 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
727 sub create_hold_notify {
728 my( $self, $conn, $auth, $note ) = @_;
729 my $e = new_editor(authtoken=>$auth, xact=>1);
730 return $e->die_event unless $e->checkauth;
732 my $hold = $e->retrieve_action_hold_request($note->hold)
733 or return $e->die_event;
734 my $patron = $e->retrieve_actor_user($hold->usr)
735 or return $e->die_event;
737 return $e->die_event unless
738 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
740 $note->notify_staff($e->requestor->id);
741 $e->create_action_hold_notification($note) or return $e->die_event;
747 __PACKAGE__->register_method(
748 method => 'reset_hold',
749 api_name => 'open-ils.circ.hold.reset',
751 Un-captures and un-targets a hold, essentially returning
752 it to the state it was in directly after it was placed,
753 then attempts to re-target the hold
754 @param authtoken The login session key
755 @param holdid The id of the hold
761 my( $self, $conn, $auth, $holdid ) = @_;
763 my ($hold, $evt) = $U->fetch_hold($holdid);
765 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
767 $evt = $self->_reset_hold($reqr, $hold);
773 my ($self, $reqr, $hold) = @_;
775 my $e = new_editor(xact =>1, requestor => $reqr);
777 $logger->info("reseting hold ".$hold->id);
781 if( $hold->capture_time and $hold->current_copy ) {
783 my $copy = $e->retrieve_asset_copy($hold->current_copy)
786 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
787 $logger->info("setting copy to status 'reshelving' on hold retarget");
788 $copy->status(OILS_COPY_STATUS_RESHELVING);
789 $copy->editor($e->requestor->id);
790 $copy->edit_date('now');
791 $e->update_asset_copy($copy) or return $e->event;
793 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
795 # We don't want the copy to remain "in transit"
796 $copy->status(OILS_COPY_STATUS_RESHELVING);
797 $logger->warn("! reseting hold [$hid] that is in transit");
798 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
801 my $trans = $e->retrieve_action_transit_copy($transid);
803 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
804 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
805 $logger->info("Transit abort completed with result $evt");
806 return $evt unless "$evt" eq 1;
812 $hold->clear_capture_time;
813 $hold->clear_current_copy;
815 $e->update_action_hold_request($hold) or return $e->event;
819 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
825 __PACKAGE__->register_method(
826 method => 'fetch_open_title_holds',
827 api_name => 'open-ils.circ.open_holds.retrieve',
829 Returns a list ids of un-fulfilled holds for a given title id
830 @param authtoken The login session key
831 @param id the id of the item whose holds we want to retrieve
832 @param type The hold type - M, T, V, C
836 sub fetch_open_title_holds {
837 my( $self, $conn, $auth, $id, $type, $org ) = @_;
838 my $e = new_editor( authtoken => $auth );
839 return $e->event unless $e->checkauth;
842 $org ||= $e->requestor->ws_ou;
844 # return $e->search_action_hold_request(
845 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
847 # XXX make me return IDs in the future ^--
848 my $holds = $e->search_action_hold_request(
851 cancel_time => undef,
853 fulfillment_time => undef
857 flesh_hold_transits($holds);
862 sub flesh_hold_transits {
864 for my $hold ( @$holds ) {
866 $apputils->simplereq(
868 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
869 { hold => $hold->id },
870 { order_by => { ahtc => 'id desc' }, limit => 1 }
876 sub flesh_hold_notices {
877 my( $holds, $e ) = @_;
880 for my $hold (@$holds) {
881 my $notices = $e->search_action_hold_notification(
883 { hold => $hold->id },
884 { order_by => { anh => 'notify_time desc' } },
889 $hold->notify_count(scalar(@$notices));
891 my $n = $e->retrieve_action_hold_notification($$notices[0])
893 $hold->notify_time($n->notify_time);
901 __PACKAGE__->register_method(
902 method => 'fetch_captured_holds',
903 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
905 Returns a list of un-fulfilled holds for a given title id
906 @param authtoken The login session key
907 @param org The org id of the location in question
911 __PACKAGE__->register_method(
912 method => 'fetch_captured_holds',
913 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
915 Returns a list ids of un-fulfilled holds for a given title id
916 @param authtoken The login session key
917 @param org The org id of the location in question
921 sub fetch_captured_holds {
922 my( $self, $conn, $auth, $org ) = @_;
924 my $e = new_editor(authtoken => $auth);
925 return $e->event unless $e->checkauth;
926 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
928 $org ||= $e->requestor->ws_ou;
930 my $holds = $e->search_action_hold_request(
932 capture_time => { "!=" => undef },
933 current_copy => { "!=" => undef },
934 fulfillment_time => undef,
936 cancel_time => undef,
941 for my $h (@$holds) {
942 my $copy = $e->retrieve_asset_copy($h->current_copy)
945 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
948 if( ! $self->api_name =~ /id_list/ ) {
949 flesh_hold_transits(\@res);
950 flesh_hold_notices(\@res, $e);
953 if( $self->api_name =~ /id_list/ ) {
954 return [ map { $_->id } @res ];
961 __PACKAGE__->register_method(
962 method => "check_title_hold",
963 api_name => "open-ils.circ.title_hold.is_possible",
965 Determines if a hold were to be placed by a given user,
966 whether or not said hold would have any potential copies
968 @param authtoken The login session key
969 @param params A hash of named params including:
970 patronid - the id of the hold recipient
971 titleid (brn) - the id of the title to be held
972 depth - the hold range depth (defaults to 0)
975 sub check_title_hold {
976 my( $self, $client, $authtoken, $params ) = @_;
978 my %params = %$params;
979 my $titleid = $params{titleid} ||"";
980 my $volid = $params{volume_id};
981 my $copyid = $params{copy_id};
982 my $mrid = $params{mrid} ||"";
983 my $depth = $params{depth} || 0;
984 my $pickup_lib = $params{pickup_lib};
985 my $hold_type = $params{hold_type} || 'T';
987 my $e = new_editor(authtoken=>$authtoken);
988 return $e->event unless $e->checkauth;
989 my $patron = $e->retrieve_actor_user($params{patronid})
992 if( $e->requestor->id ne $patron->id ) {
993 return $e->event unless
994 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
997 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
999 my $rangelib = $params{range_lib} || $patron->home_ou;
1001 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1002 or return $e->event;
1004 $logger->info("checking hold possibility with type $hold_type");
1010 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1012 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1013 $volume = $e->retrieve_asset_call_number($copy->call_number)
1014 or return $e->event;
1015 $title = $e->retrieve_biblio_record_entry($volume->record)
1016 or return $e->event;
1017 return verify_copy_for_hold(
1018 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1020 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1022 $volume = $e->retrieve_asset_call_number($volid)
1023 or return $e->event;
1024 $title = $e->retrieve_biblio_record_entry($volume->record)
1025 or return $e->event;
1027 return _check_volume_hold_is_possible(
1028 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1030 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1032 return _check_title_hold_is_possible(
1033 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1035 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1037 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1038 my @recs = map { $_->source } @$maps;
1039 for my $rec (@recs) {
1040 return 1 if (_check_title_hold_is_possible(
1041 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1049 sub ___check_title_hold_is_possible {
1050 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1056 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1058 while( $title = $U->storagereq(
1059 'open-ils.storage.biblio.record_entry.ranged_tree',
1060 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1064 ref($title->call_numbers) and
1065 @{$title->call_numbers};
1067 for my $cn (@{$title->call_numbers}) {
1069 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1071 for my $copy (@{$cn->copies}) {
1072 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1073 return 1 if verify_copy_for_hold(
1074 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1075 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1086 sub _check_title_hold_is_possible {
1087 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1089 my $e = new_editor();
1092 my $copies = $e->search_asset_copy(
1094 { deleted => 'f', circulate => 't', holdable => 't' },
1099 filter => { holdable => 't' }
1104 filter => { holdable => 't' }
1108 fkey => 'call_number',
1113 filter => { id => $titleid }
1123 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1124 my $copies = $e->json_query(
1126 select => { acp => ['id', 'circ_lib'] },
1131 fkey => 'call_number',
1135 filter => { id => $titleid },
1140 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1141 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1145 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1150 return $e->event unless defined $copies;
1151 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1152 return 0 unless @$copies;
1154 # -----------------------------------------------------------------------
1155 # sort the copies into buckets based on their circ_lib proximity to
1156 # the patron's home_ou.
1157 # -----------------------------------------------------------------------
1159 my $home_org = $patron->home_ou;
1160 my $req_org = $request_lib->id;
1163 ($prox_cache{$home_org}) ?
1164 $prox_cache{$home_org} :
1165 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1168 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1169 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1171 my @keys = sort { $a <=> $b } keys %buckets;
1174 if( $home_org ne $req_org ) {
1175 # -----------------------------------------------------------------------
1176 # shove the copies close to the request_lib into the primary buckets
1177 # directly before the farthest away copies. That way, they are not
1178 # given priority, but they are checked before the farthest copies.
1179 # -----------------------------------------------------------------------
1181 ($prox_cache{$req_org}) ?
1182 $prox_cache{$req_org} :
1183 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1186 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1187 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1189 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1190 my $new_key = $highest_key - 0.5; # right before the farthest prox
1191 my @keys2 = sort { $a <=> $b } keys %buckets2;
1192 for my $key (@keys2) {
1193 last if $key >= $highest_key;
1194 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1198 @keys = sort { $a <=> $b } keys %buckets;
1202 for my $key (@keys) {
1203 my @cps = @{$buckets{$key}};
1205 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1207 for my $copyid (@cps) {
1209 next if $seen{$copyid};
1210 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1211 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1212 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1214 unless($title) { # grab the title if we don't already have it
1215 my $vol = $e->retrieve_asset_call_number(
1216 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1217 $title = $vol->record;
1220 return 1 if verify_copy_for_hold(
1221 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1230 sub _check_volume_hold_is_possible {
1231 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1232 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1233 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1234 for my $copy ( @$copies ) {
1235 return 1 if verify_copy_for_hold(
1236 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1243 sub verify_copy_for_hold {
1244 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1245 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1246 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1247 { patron => $patron,
1248 requestor => $requestor,
1251 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1252 pickup_lib => $pickup_lib,
1253 request_lib => $request_lib
1261 sub find_nearest_permitted_hold {
1264 my $editor = shift; # CStoreEditor object
1265 my $copy = shift; # copy to target
1266 my $user = shift; # hold recipient
1267 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1268 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1270 my $bc = $copy->barcode;
1272 # find any existing holds that already target this copy
1273 my $old_holds = $editor->search_action_hold_request(
1274 { current_copy => $copy->id,
1275 cancel_time => undef,
1276 capture_time => undef
1280 # hold->type "R" means we need this copy
1281 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1283 $logger->info("circulator: searching for best hold at org ".$user->ws_ou." and copy $bc");
1285 # search for what should be the best holds for this copy to fulfill
1286 my $best_holds = $U->storagereq(
1287 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1288 $user->ws_ou, $copy->id, 10 );
1290 unless(@$best_holds) {
1292 if( my $hold = $$old_holds[0] ) {
1293 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1297 $logger->info("circulator: no suitable holds found for copy $bc");
1298 return (undef, $evt);
1304 # for each potential hold, we have to run the permit script
1305 # to make sure the hold is actually permitted.
1306 for my $holdid (@$best_holds) {
1307 next unless $holdid;
1308 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1310 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1311 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1312 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1314 # see if this hold is permitted
1315 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1316 { patron_id => $hold->usr,
1317 requestor => $reqr->id,
1319 pickup_lib => $hold->pickup_lib,
1320 request_lib => $rlib,
1331 unless( $best_hold ) { # no "good" permitted holds were found
1332 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1333 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1338 $logger->info("circulator: no suitable holds found for copy $bc");
1339 return (undef, $evt);
1342 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1344 # indicate a permitted hold was found
1345 return $best_hold if $check_only;
1347 # we've found a permitted hold. we need to "grab" the copy
1348 # to prevent re-targeted holds (next part) from re-grabbing the copy
1349 $best_hold->current_copy($copy->id);
1350 $editor->update_action_hold_request($best_hold)
1351 or return (undef, $editor->event);
1354 # re-target any other holds that already target this copy
1355 for my $old_hold (@$old_holds) {
1356 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1357 $logger->info("circulator: re-targeting hold ".$old_hold->id.
1358 " after a better hold [".$best_hold->id."] was found");
1360 'open-ils.storage.action.hold_request.copy_targeter', undef, $old_hold->id );
1363 return ($best_hold);
1371 __PACKAGE__->register_method(
1372 method => 'all_rec_holds',
1373 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1377 my( $self, $conn, $auth, $title_id, $args ) = @_;
1379 my $e = new_editor(authtoken=>$auth);
1380 $e->checkauth or return $e->event;
1381 $e->allowed('VIEW_HOLD') or return $e->event;
1383 $args ||= { fulfillment_time => undef };
1384 $args->{cancel_time} = undef;
1386 my $resp = { volume_holds => [], copy_holds => [] };
1388 $resp->{title_holds} = $e->search_action_hold_request(
1390 hold_type => OILS_HOLD_TYPE_TITLE,
1391 target => $title_id,
1395 my $vols = $e->search_asset_call_number(
1396 { record => $title_id, deleted => 'f' }, {idlist=>1});
1398 return $resp unless @$vols;
1400 $resp->{volume_holds} = $e->search_action_hold_request(
1402 hold_type => OILS_HOLD_TYPE_VOLUME,
1407 my $copies = $e->search_asset_copy(
1408 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1410 return $resp unless @$copies;
1412 $resp->{copy_holds} = $e->search_action_hold_request(
1414 hold_type => OILS_HOLD_TYPE_COPY,
1426 __PACKAGE__->register_method(
1427 method => 'uber_hold',
1428 api_name => 'open-ils.circ.hold.details.retrieve'
1432 my($self, $client, $auth, $hold_id) = @_;
1433 my $e = new_editor(authtoken=>$auth);
1434 $e->checkauth or return $e->event;
1435 $e->allowed('VIEW_HOLD') or return $e->event;
1439 my $hold = $e->retrieve_action_hold_request(
1444 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1447 ) or return $e->event;
1449 my $user = $hold->usr;
1450 $hold->usr($user->id);
1452 my $card = $e->retrieve_actor_card($user->card)
1453 or return $e->event;
1455 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1457 flesh_hold_notices([$hold], $e);
1458 flesh_hold_transits([$hold]);
1465 status => _hold_status($e, $hold),
1466 patron_first => $user->first_given_name,
1467 patron_last => $user->family_name,
1468 patron_barcode => $card->barcode,
1474 # -----------------------------------------------------
1475 # Returns the MVR object that represents what the
1477 # -----------------------------------------------------
1479 my( $e, $hold ) = @_;
1485 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1486 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1487 or return $e->event;
1488 $tid = $mr->master_record;
1490 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1491 $tid = $hold->target;
1493 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1494 $volume = $e->retrieve_asset_call_number($hold->target)
1495 or return $e->event;
1496 $tid = $volume->record;
1498 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1499 $copy = $e->retrieve_asset_copy($hold->target)
1500 or return $e->event;
1501 $volume = $e->retrieve_asset_call_number($copy->call_number)
1502 or return $e->event;
1503 $tid = $volume->record;
1506 if(!$copy and ref $hold->current_copy ) {
1507 $copy = $hold->current_copy;
1508 $hold->current_copy($copy->id);
1511 if(!$volume and $copy) {
1512 $volume = $e->retrieve_asset_call_number($copy->call_number);
1515 my $title = $e->retrieve_biblio_record_entry($tid);
1516 return ( $U->record_to_mvr($title), $volume, $copy );