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 ];
366 __PACKAGE__->register_method(
367 method => "retrieve_holds_by_pickup_lib",
368 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
370 Retrieves all the holds, with hold transits attached, for the specified
374 __PACKAGE__->register_method(
375 method => "retrieve_holds_by_pickup_lib",
376 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
378 Retrieves all the hold ids for the specified
382 sub retrieve_holds_by_pickup_lib {
383 my($self, $client, $login_session, $ou_id) = @_;
385 #FIXME -- put an appropriate permission check here
386 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
387 # $login_session, $user_id, 'VIEW_HOLD' );
388 #return $evt if $evt;
390 my $holds = $apputils->simplereq(
392 "open-ils.cstore.direct.action.hold_request.search.atomic",
394 pickup_lib => $ou_id ,
395 fulfillment_time => undef,
398 { order_by => { ahr => "request_time" } });
401 if( ! $self->api_name =~ /id_list/ ) {
402 flesh_hold_transits($holds);
405 if( $self->api_name =~ /id_list/ ) {
406 return [ map { $_->id } @$holds ];
412 __PACKAGE__->register_method(
413 method => "cancel_hold",
414 api_name => "open-ils.circ.hold.cancel",
416 Cancels the specified hold. The login session
417 is the requestor and if the requestor is different from the usr field
418 on the hold, the requestor must have CANCEL_HOLDS permissions.
419 the hold may be either the hold object or the hold id
423 my($self, $client, $auth, $holdid) = @_;
425 my $e = new_editor(authtoken=>$auth, xact=>1);
426 return $e->event unless $e->checkauth;
428 my $hold = $e->retrieve_action_hold_request($holdid)
431 if( $e->requestor->id ne $hold->usr ) {
432 return $e->event unless $e->allowed('CANCEL_HOLDS');
435 return 1 if $hold->cancel_time;
437 # If the hold is captured, reset the copy status
438 if( $hold->capture_time and $hold->current_copy ) {
440 my $copy = $e->retrieve_asset_copy($hold->current_copy)
443 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
444 $logger->info("setting copy to status 'reshelving' on hold cancel");
445 $copy->status(OILS_COPY_STATUS_RESHELVING);
446 $copy->editor($e->requestor->id);
447 $copy->edit_date('now');
448 $e->update_asset_copy($copy) or return $e->event;
450 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
453 $logger->warn("! canceling hold [$hid] that is in transit");
454 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
457 my $trans = $e->retrieve_action_transit_copy($transid);
458 # Leave the transit alive, but set the copy status to
459 # reshelving so it will be properly reshelved when it gets back home
461 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
462 $e->update_action_transit_copy($trans) or return $e->die_event;
469 $hold->cancel_time('now');
470 $e->update_action_hold_request($hold)
473 $self->delete_hold_copy_maps($e, $hold->id);
479 sub delete_hold_copy_maps {
484 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
486 $editor->delete_action_hold_copy_map($_)
487 or return $editor->event;
493 __PACKAGE__->register_method(
494 method => "update_hold",
495 api_name => "open-ils.circ.hold.update",
497 Updates the specified hold. The login session
498 is the requestor and if the requestor is different from the usr field
499 on the hold, the requestor must have UPDATE_HOLDS permissions.
503 my($self, $client, $login_session, $hold) = @_;
505 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
506 $login_session, $hold->usr, 'UPDATE_HOLD' );
509 $logger->activity('User ' . $requestor->id .
510 ' updating hold ' . $hold->id . ' for user ' . $target->id );
512 return $U->storagereq(
513 "open-ils.storage.direct.action.hold_request.update", $hold );
517 __PACKAGE__->register_method(
518 method => "retrieve_hold_status",
519 api_name => "open-ils.circ.hold.status.retrieve",
521 Calculates the current status of the hold.
522 the requestor must have VIEW_HOLD permissions if the hold is for a user
523 other than the requestor.
524 Returns -1 on error (for now)
525 Returns 1 for 'waiting for copy to become available'
526 Returns 2 for 'waiting for copy capture'
527 Returns 3 for 'in transit'
528 Returns 4 for 'arrived'
531 sub retrieve_hold_status {
532 my($self, $client, $auth, $hold_id) = @_;
534 my $e = new_editor(authtoken => $auth);
535 return $e->event unless $e->checkauth;
536 my $hold = $e->retrieve_action_hold_request($hold_id)
539 if( $e->requestor->id != $hold->usr ) {
540 return $e->event unless $e->allowed('VIEW_HOLD');
543 return _hold_status($e, $hold);
549 return 1 unless $hold->current_copy;
550 return 2 unless $hold->capture_time;
552 my $copy = $hold->current_copy;
553 unless( ref $copy ) {
554 $copy = $e->retrieve_asset_copy($hold->current_copy)
558 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
559 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
565 #sub find_local_hold {
566 # my( $class, $session, $copy, $user ) = @_;
567 # return $class->find_nearest_permitted_hold($session, $copy, $user);
571 sub fetch_open_hold_by_current_copy {
574 my $hold = $apputils->simplereq(
576 'open-ils.cstore.direct.action.hold_request.search.atomic',
577 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
578 return $hold->[0] if ref($hold);
582 sub fetch_related_holds {
585 return $apputils->simplereq(
587 'open-ils.cstore.direct.action.hold_request.search.atomic',
588 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
592 __PACKAGE__->register_method (
593 method => "hold_pull_list",
594 api_name => "open-ils.circ.hold_pull_list.retrieve",
596 Returns a list of holds that need to be "pulled"
601 __PACKAGE__->register_method (
602 method => "hold_pull_list",
603 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
605 Returns a list of hold ID's that need to be "pulled"
612 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
613 my( $reqr, $evt ) = $U->checkses($authtoken);
616 my $org = $reqr->ws_ou || $reqr->home_ou;
617 # the perm locaiton shouldn't really matter here since holds
618 # will exist all over and VIEW_HOLDS should be universal
619 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
622 if( $self->api_name =~ /id_list/ ) {
623 return $U->storagereq(
624 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
625 $org, $limit, $offset );
627 return $U->storagereq(
628 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
629 $org, $limit, $offset );
633 __PACKAGE__->register_method (
634 method => 'fetch_hold_notify',
635 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
637 Returns a list of hold notification objects based on hold id.
638 @param authtoken The loggin session key
639 @param holdid The id of the hold whose notifications we want to retrieve
640 @return An array of hold notification objects, event on error.
644 sub fetch_hold_notify {
645 my( $self, $conn, $authtoken, $holdid ) = @_;
646 my( $requestor, $evt ) = $U->checkses($authtoken);
649 ($hold, $evt) = $U->fetch_hold($holdid);
651 ($patron, $evt) = $U->fetch_user($hold->usr);
654 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
657 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
658 return $U->cstorereq(
659 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
663 __PACKAGE__->register_method (
664 method => 'create_hold_notify',
665 api_name => 'open-ils.circ.hold_notification.create',
667 Creates a new hold notification object
668 @param authtoken The login session key
669 @param notification The hold notification object to create
670 @return ID of the new object on success, Event on error
674 sub __create_hold_notify {
675 my( $self, $conn, $authtoken, $notification ) = @_;
676 my( $requestor, $evt ) = $U->checkses($authtoken);
679 ($hold, $evt) = $U->fetch_hold($notification->hold);
681 ($patron, $evt) = $U->fetch_user($hold->usr);
684 # XXX perm depth probably doesn't matter here -- should always be consortium level
685 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
688 # Set the proper notifier
689 $notification->notify_staff($requestor->id);
690 my $id = $U->storagereq(
691 'open-ils.storage.direct.action.hold_notification.create', $notification );
692 return $U->DB_UPDATE_FAILED($notification) unless $id;
693 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
698 sub create_hold_notify {
699 my( $self, $conn, $auth, $note ) = @_;
700 my $e = new_editor(authtoken=>$auth, xact=>1);
701 return $e->die_event unless $e->checkauth;
703 my $hold = $e->retrieve_action_hold_request($note->hold)
704 or return $e->die_event;
705 my $patron = $e->retrieve_actor_user($hold->usr)
706 or return $e->die_event;
708 return $e->die_event unless
709 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
711 $note->notify_staff($e->requestor->id);
712 $e->create_action_hold_notification($note) or return $e->die_event;
718 __PACKAGE__->register_method(
719 method => 'reset_hold',
720 api_name => 'open-ils.circ.hold.reset',
722 Un-captures and un-targets a hold, essentially returning
723 it to the state it was in directly after it was placed,
724 then attempts to re-target the hold
725 @param authtoken The login session key
726 @param holdid The id of the hold
732 my( $self, $conn, $auth, $holdid ) = @_;
734 my ($hold, $evt) = $U->fetch_hold($holdid);
736 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
738 $evt = $self->_reset_hold($reqr, $hold);
744 my ($self, $reqr, $hold) = @_;
746 my $e = new_editor(xact =>1, requestor => $reqr);
748 $logger->info("reseting hold ".$hold->id);
752 if( $hold->capture_time and $hold->current_copy ) {
754 my $copy = $e->retrieve_asset_copy($hold->current_copy)
757 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
758 $logger->info("setting copy to status 'reshelving' on hold retarget");
759 $copy->status(OILS_COPY_STATUS_RESHELVING);
760 $copy->editor($e->requestor->id);
761 $copy->edit_date('now');
762 $e->update_asset_copy($copy) or return $e->event;
764 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
766 # We don't want the copy to remain "in transit"
767 $copy->status(OILS_COPY_STATUS_RESHELVING);
768 $logger->warn("! reseting hold [$hid] that is in transit");
769 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
772 my $trans = $e->retrieve_action_transit_copy($transid);
774 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
775 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
776 $logger->info("Transit abort completed with result $evt");
777 return $evt unless "$evt" eq 1;
783 $hold->clear_capture_time;
784 $hold->clear_current_copy;
786 $e->update_action_hold_request($hold) or return $e->event;
790 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
796 __PACKAGE__->register_method(
797 method => 'fetch_open_title_holds',
798 api_name => 'open-ils.circ.open_holds.retrieve',
800 Returns a list ids of un-fulfilled holds for a given title id
801 @param authtoken The login session key
802 @param id the id of the item whose holds we want to retrieve
803 @param type The hold type - M, T, V, C
807 sub fetch_open_title_holds {
808 my( $self, $conn, $auth, $id, $type, $org ) = @_;
809 my $e = new_editor( authtoken => $auth );
810 return $e->event unless $e->checkauth;
813 $org ||= $e->requestor->ws_ou;
815 # return $e->search_action_hold_request(
816 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
818 # XXX make me return IDs in the future ^--
819 my $holds = $e->search_action_hold_request(
822 cancel_time => undef,
824 fulfillment_time => undef
828 flesh_hold_transits($holds);
833 sub flesh_hold_transits {
835 for my $hold ( @$holds ) {
837 $apputils->simplereq(
839 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
840 { hold => $hold->id },
841 { order_by => { ahtc => 'id desc' }, limit => 1 }
847 sub flesh_hold_notices {
848 my( $holds, $e ) = @_;
851 for my $hold (@$holds) {
852 my $notices = $e->search_action_hold_notification(
854 { hold => $hold->id },
855 { order_by => { anh => 'notify_time desc' } },
860 $hold->notify_count(scalar(@$notices));
862 my $n = $e->retrieve_action_hold_notification($$notices[0])
864 $hold->notify_time($n->notify_time);
872 __PACKAGE__->register_method(
873 method => 'fetch_captured_holds',
874 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
876 Returns a list of un-fulfilled holds for a given title id
877 @param authtoken The login session key
878 @param org The org id of the location in question
882 __PACKAGE__->register_method(
883 method => 'fetch_captured_holds',
884 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
886 Returns a list ids of un-fulfilled holds for a given title id
887 @param authtoken The login session key
888 @param org The org id of the location in question
892 sub fetch_captured_holds {
893 my( $self, $conn, $auth, $org ) = @_;
895 my $e = new_editor(authtoken => $auth);
896 return $e->event unless $e->checkauth;
897 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
899 $org ||= $e->requestor->ws_ou;
901 my $holds = $e->search_action_hold_request(
903 capture_time => { "!=" => undef },
904 current_copy => { "!=" => undef },
905 fulfillment_time => undef,
907 cancel_time => undef,
912 for my $h (@$holds) {
913 my $copy = $e->retrieve_asset_copy($h->current_copy)
916 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
919 if( ! $self->api_name =~ /id_list/ ) {
920 flesh_hold_transits(\@res);
921 flesh_hold_notices(\@res, $e);
924 if( $self->api_name =~ /id_list/ ) {
925 return [ map { $_->id } @res ];
932 __PACKAGE__->register_method(
933 method => "check_title_hold",
934 api_name => "open-ils.circ.title_hold.is_possible",
936 Determines if a hold were to be placed by a given user,
937 whether or not said hold would have any potential copies
939 @param authtoken The login session key
940 @param params A hash of named params including:
941 patronid - the id of the hold recipient
942 titleid (brn) - the id of the title to be held
943 depth - the hold range depth (defaults to 0)
946 sub check_title_hold {
947 my( $self, $client, $authtoken, $params ) = @_;
949 my %params = %$params;
950 my $titleid = $params{titleid} ||"";
951 my $volid = $params{volume_id};
952 my $copyid = $params{copy_id};
953 my $mrid = $params{mrid} ||"";
954 my $depth = $params{depth} || 0;
955 my $pickup_lib = $params{pickup_lib};
956 my $hold_type = $params{hold_type} || 'T';
958 my $e = new_editor(authtoken=>$authtoken);
959 return $e->event unless $e->checkauth;
960 my $patron = $e->retrieve_actor_user($params{patronid})
963 if( $e->requestor->id ne $patron->id ) {
964 return $e->event unless
965 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
968 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
970 my $rangelib = $params{range_lib} || $patron->home_ou;
972 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
975 $logger->info("checking hold possibility with type $hold_type");
981 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
983 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
984 $volume = $e->retrieve_asset_call_number($copy->call_number)
986 $title = $e->retrieve_biblio_record_entry($volume->record)
988 return verify_copy_for_hold(
989 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
991 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
993 $volume = $e->retrieve_asset_call_number($volid)
995 $title = $e->retrieve_biblio_record_entry($volume->record)
998 return _check_volume_hold_is_possible(
999 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1001 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1003 return _check_title_hold_is_possible(
1004 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1006 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1008 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1009 my @recs = map { $_->source } @$maps;
1010 for my $rec (@recs) {
1011 return 1 if (_check_title_hold_is_possible(
1012 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1020 sub _check_title_hold_is_possible {
1021 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1027 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1029 while( $title = $U->storagereq(
1030 'open-ils.storage.biblio.record_entry.ranged_tree',
1031 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1035 ref($title->call_numbers) and
1036 @{$title->call_numbers};
1038 for my $cn (@{$title->call_numbers}) {
1040 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1042 for my $copy (@{$cn->copies}) {
1043 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1044 return 1 if verify_copy_for_hold(
1045 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1046 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1055 sub _check_volume_hold_is_possible {
1056 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1057 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1058 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1059 for my $copy ( @$copies ) {
1060 return 1 if verify_copy_for_hold(
1061 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1068 sub verify_copy_for_hold {
1069 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1070 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1071 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1072 { patron => $patron,
1073 requestor => $requestor,
1076 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1077 pickup_lib => $pickup_lib,
1078 request_lib => $request_lib
1086 sub find_nearest_permitted_hold {
1089 my $editor = shift; # CStoreEditor object
1090 my $copy = shift; # copy to target
1091 my $user = shift; # hold recipient
1092 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1093 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1095 my $bc = $copy->barcode;
1097 # find any existing holds that already target this copy
1098 my $old_holds = $editor->search_action_hold_request(
1099 { current_copy => $copy->id,
1100 cancel_time => undef,
1101 capture_time => undef
1105 # hold->type "R" means we need this copy
1106 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1108 $logger->info("circulator: searching for best hold at org ".$user->ws_ou." and copy $bc");
1110 # search for what should be the best holds for this copy to fulfill
1111 my $best_holds = $U->storagereq(
1112 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1113 $user->ws_ou, $copy->id, 10 );
1115 unless(@$best_holds) {
1117 if( my $hold = $$old_holds[0] ) {
1118 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1122 $logger->info("circulator: no suitable holds found for copy $bc");
1123 return (undef, $evt);
1129 # for each potential hold, we have to run the permit script
1130 # to make sure the hold is actually permitted.
1131 for my $holdid (@$best_holds) {
1132 next unless $holdid;
1133 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1135 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1136 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1137 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1139 # see if this hold is permitted
1140 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1141 { patron_id => $hold->usr,
1142 requestor => $reqr->id,
1144 pickup_lib => $hold->pickup_lib,
1145 request_lib => $rlib,
1156 unless( $best_hold ) { # no "good" permitted holds were found
1157 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1158 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1163 $logger->info("circulator: no suitable holds found for copy $bc");
1164 return (undef, $evt);
1167 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1169 # indicate a permitted hold was found
1170 return $best_hold if $check_only;
1172 # we've found a permitted hold. we need to "grab" the copy
1173 # to prevent re-targeted holds (next part) from re-grabbing the copy
1174 $best_hold->current_copy($copy->id);
1175 $editor->update_action_hold_request($best_hold)
1176 or return (undef, $editor->event);
1179 # re-target any other holds that already target this copy
1180 for my $old_hold (@$old_holds) {
1181 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1182 $logger->info("circulator: re-targeting hold ".$old_hold->id.
1183 " after a better hold [".$best_hold->id."] was found");
1185 'open-ils.storage.action.hold_request.copy_targeter', undef, $old_hold->id );
1188 return ($best_hold);
1196 __PACKAGE__->register_method(
1197 method => 'all_rec_holds',
1198 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1202 my( $self, $conn, $auth, $title_id, $args ) = @_;
1204 my $e = new_editor(authtoken=>$auth);
1205 $e->checkauth or return $e->event;
1206 $e->allowed('VIEW_HOLD') or return $e->event;
1208 $args ||= { fulfillment_time => undef };
1209 $args->{cancel_time} = undef;
1211 my $resp = { volume_holds => [], copy_holds => [] };
1213 $resp->{title_holds} = $e->search_action_hold_request(
1215 hold_type => OILS_HOLD_TYPE_TITLE,
1216 target => $title_id,
1220 my $vols = $e->search_asset_call_number(
1221 { record => $title_id, deleted => 'f' }, {idlist=>1});
1223 return $resp unless @$vols;
1225 $resp->{volume_holds} = $e->search_action_hold_request(
1227 hold_type => OILS_HOLD_TYPE_VOLUME,
1232 my $copies = $e->search_asset_copy(
1233 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1235 return $resp unless @$copies;
1237 $resp->{copy_holds} = $e->search_action_hold_request(
1239 hold_type => OILS_HOLD_TYPE_COPY,
1251 __PACKAGE__->register_method(
1252 method => 'uber_hold',
1253 api_name => 'open-ils.circ.hold.details.retrieve'
1257 my($self, $client, $auth, $hold_id) = @_;
1258 my $e = new_editor(authtoken=>$auth);
1259 $e->checkauth or return $e->event;
1260 $e->allowed('VIEW_HOLD') or return $e->event;
1264 my $hold = $e->retrieve_action_hold_request(
1269 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1272 ) or return $e->event;
1274 my $user = $hold->usr;
1275 $hold->usr($user->id);
1277 my $card = $e->retrieve_actor_card($user->card)
1278 or return $e->event;
1280 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1282 flesh_hold_notices([$hold], $e);
1283 flesh_hold_transits([$hold]);
1290 status => _hold_status($e, $hold),
1291 patron_first => $user->first_given_name,
1292 patron_last => $user->family_name,
1293 patron_barcode => $card->barcode,
1299 # -----------------------------------------------------
1300 # Returns the MVR object that represents what the
1302 # -----------------------------------------------------
1304 my( $e, $hold ) = @_;
1310 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1311 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1312 or return $e->event;
1313 $tid = $mr->master_record;
1315 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1316 $tid = $hold->target;
1318 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1319 $volume = $e->retrieve_asset_call_number($hold->target)
1320 or return $e->event;
1321 $tid = $volume->record;
1323 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1324 $copy = $e->retrieve_asset_copy($hold->target)
1325 or return $e->event;
1326 $volume = $e->retrieve_asset_call_number($copy->call_number)
1327 or return $e->event;
1328 $tid = $volume->record;
1331 if(!$copy and ref $hold->current_copy ) {
1332 $copy = $hold->current_copy;
1333 $hold->current_copy($copy->id);
1336 if(!$volume and $copy) {
1337 $volume = $e->retrieve_asset_call_number($copy->call_number);
1340 my $title = $e->retrieve_biblio_record_entry($tid);
1341 return ( $U->record_to_mvr($title), $volume, $copy );