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/;
31 my $apputils = "OpenILS::Application::AppUtils";
36 __PACKAGE__->register_method(
37 method => "create_hold",
38 api_name => "open-ils.circ.holds.create",
40 Create a new hold for an item. From a permissions perspective,
41 the login session is used as the 'requestor' of the hold.
42 The hold recipient is determined by the 'usr' setting within
45 First we verify the requestion has holds request permissions.
46 Then we verify that the recipient is allowed to make the given hold.
47 If not, we see if the requestor has "override" capabilities. If not,
48 a permission exception is returned. If permissions allow, we cycle
49 through the set of holds objects and create.
51 If the recipient does not have permission to place multiple holds
52 on a single title and said operation is attempted, a permission
57 __PACKAGE__->register_method(
58 method => "create_hold",
59 api_name => "open-ils.circ.holds.create.override",
61 If the recipient is not allowed to receive the requested hold,
62 call this method to attempt the override
63 @see open-ils.circ.holds.create
68 my( $self, $conn, $auth, @holds ) = @_;
69 my $e = new_editor(authtoken=>$auth, xact=>1);
70 return $e->event unless $e->checkauth;
72 my $override = 1 if $self->api_name =~ /override/;
74 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
78 for my $hold (@$holds) {
83 my $requestor = $e->requestor;
84 my $recipient = $requestor;
87 if( $requestor->id ne $hold->usr ) {
88 # Make sure the requestor is allowed to place holds for
89 # the recipient if they are not the same people
90 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
91 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
94 # Now make sure the recipient is allowed to receive the specified hold
96 my $porg = $recipient->home_ou;
97 my $rid = $e->requestor->id;
98 my $t = $hold->hold_type;
100 # See if a duplicate hold already exists
102 usr => $recipient->id,
104 fulfillment_time => undef,
105 target => $hold->target,
106 cancel_time => undef,
109 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
111 my $existing = $e->search_action_hold_request($sargs);
112 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
114 if( $t eq OILS_HOLD_TYPE_METARECORD )
115 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'MR_HOLDS'); }
117 if( $t eq OILS_HOLD_TYPE_TITLE )
118 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS'); }
120 if( $t eq OILS_HOLD_TYPE_VOLUME )
121 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
123 if( $t eq OILS_HOLD_TYPE_COPY )
124 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
126 return $pevt if $pevt;
130 for my $evt (@events) {
132 my $name = $evt->{textcode};
133 return $e->event unless $e->allowed("$name.override", $porg);
140 $hold->requestor($e->requestor->id);
141 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
142 $hold = $e->create_action_hold_request($hold) or return $e->event;
143 push( @copyholds, $hold ) if $hold->hold_type eq OILS_HOLD_TYPE_COPY;
148 # Go ahead and target the copy-level holds
150 'open-ils.storage.action.hold_request.copy_targeter',
151 undef, $_->id ) for @copyholds;
157 my( $self, $client, $login_session, @holds) = @_;
159 if(!@holds){return 0;}
160 my( $user, $evt ) = $apputils->checkses($login_session);
164 if(ref($holds[0]) eq 'ARRAY') {
166 } else { $holds = [ @holds ]; }
168 $logger->debug("Iterating over holds requests...");
170 for my $hold (@$holds) {
173 my $type = $hold->hold_type;
175 $logger->activity("User " . $user->id .
176 " creating new hold of type $type for user " . $hold->usr);
179 if($user->id ne $hold->usr) {
180 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
190 # am I allowed to place holds for this user?
191 if($hold->requestor ne $hold->usr) {
192 $perm = _check_request_holds_perm($user->id, $user->home_ou);
193 if($perm) { return $perm; }
196 # is this user allowed to have holds of this type?
197 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
199 #if there is a requestor, see if the requestor has override privelages
200 if($hold->requestor ne $hold->usr) {
201 $perm = _check_request_holds_override($user->id, $user->home_ou);
202 if($perm) {return $perm;}
209 #enforce the fact that the login is the one requesting the hold
210 $hold->requestor($user->id);
211 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
213 my $resp = $apputils->simplereq(
215 'open-ils.storage.direct.action.hold_request.create', $hold );
218 return OpenSRF::EX::ERROR ("Error creating hold");
225 # makes sure that a user has permission to place the type of requested hold
226 # returns the Perm exception if not allowed, returns undef if all is well
227 sub _check_holds_perm {
228 my($type, $user_id, $org_id) = @_;
232 if($evt = $apputils->check_perms(
233 $user_id, $org_id, "MR_HOLDS")) {
237 } elsif ($type eq "T") {
238 if($evt = $apputils->check_perms(
239 $user_id, $org_id, "TITLE_HOLDS")) {
243 } elsif($type eq "V") {
244 if($evt = $apputils->check_perms(
245 $user_id, $org_id, "VOLUME_HOLDS")) {
249 } elsif($type eq "C") {
250 if($evt = $apputils->check_perms(
251 $user_id, $org_id, "COPY_HOLDS")) {
259 # tests if the given user is allowed to place holds on another's behalf
260 sub _check_request_holds_perm {
263 if(my $evt = $apputils->check_perms(
264 $user_id, $org_id, "REQUEST_HOLDS")) {
269 sub _check_request_holds_override {
272 if(my $evt = $apputils->check_perms(
273 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
278 __PACKAGE__->register_method(
279 method => "retrieve_holds_by_id",
280 api_name => "open-ils.circ.holds.retrieve_by_id",
282 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
283 different from the user, then the requestor must have VIEW_HOLD permissions.
287 sub retrieve_holds_by_id {
288 my($self, $client, $login_session, $hold_id) = @_;
291 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
292 # $login_session, $user_id, 'VIEW_HOLD' );
293 #return $evt if $evt;
295 my $holds = $apputils->simplereq(
297 "open-ils.cstore.direct.action.hold_request.search.atomic",
298 { id => $hold_id , fulfillment_time => undef },
299 { order_by => { ahr => "request_time" } }
302 flesh_hold_transits($holds);
307 __PACKAGE__->register_method(
308 method => "retrieve_holds",
309 api_name => "open-ils.circ.holds.retrieve",
311 Retrieves all the holds, with hold transits attached, for the specified
312 user id. The login session is the requestor and if the requestor is
313 different from the user, then the requestor must have VIEW_HOLD permissions.
318 my($self, $client, $login_session, $user_id) = @_;
320 my( $user, $target, $evt ) = $apputils->checkses_requestor(
321 $login_session, $user_id, 'VIEW_HOLD' );
324 my $holds = $apputils->simplereq(
326 "open-ils.cstore.direct.action.hold_request.search.atomic",
329 fulfillment_time => undef,
330 cancel_time => undef,
332 { order_by => { ahr => "request_time" } }
335 for my $hold ( @$holds ) {
337 $apputils->simplereq(
339 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
340 { hold => $hold->id },
341 { order_by => { ahtc => 'id desc' }, limit => 1 }
349 __PACKAGE__->register_method(
350 method => "retrieve_holds_by_pickup_lib",
351 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
353 Retrieves all the holds, with hold transits attached, for the specified
358 sub retrieve_holds_by_pickup_lib {
359 my($self, $client, $login_session, $ou_id) = @_;
361 #FIXME -- put an appropriate permission check here
362 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
363 # $login_session, $user_id, 'VIEW_HOLD' );
364 #return $evt if $evt;
366 my $holds = $apputils->simplereq(
368 "open-ils.cstore.direct.action.hold_request.search.atomic",
370 pickup_lib => $ou_id ,
371 fulfillment_time => undef,
374 { order_by => { ahr => "request_time" } });
377 flesh_hold_transits($holds);
382 __PACKAGE__->register_method(
383 method => "cancel_hold",
384 api_name => "open-ils.circ.hold.cancel",
386 Cancels the specified hold. The login session
387 is the requestor and if the requestor is different from the usr field
388 on the hold, the requestor must have CANCEL_HOLDS permissions.
389 the hold may be either the hold object or the hold id
393 my($self, $client, $auth, $holdid) = @_;
395 my $e = new_editor(authtoken=>$auth, xact=>1);
396 return $e->event unless $e->checkauth;
398 my $hold = $e->retrieve_action_hold_request($holdid)
401 if( $e->requestor->id ne $hold->usr ) {
402 return $e->event unless $e->allowed('CANCEL_HOLDS');
405 return 1 if $hold->cancel_time;
407 # If the hold is captured, reset the copy status
408 if( $hold->capture_time and $hold->current_copy ) {
410 my $copy = $e->retrieve_asset_copy($hold->current_copy)
413 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
414 $logger->info("setting copy to status 'reshelving' on hold cancel");
415 $copy->status(OILS_COPY_STATUS_RESHELVING);
416 $copy->editor($e->requestor->id);
417 $copy->edit_date('now');
418 $e->update_asset_copy($copy) or return $e->event;
422 $hold->cancel_time('now');
423 $e->update_action_hold_request($hold)
426 $self->delete_hold_copy_maps($e, $hold->id);
432 sub delete_hold_copy_maps {
437 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
439 $editor->delete_action_hold_copy_map($_)
440 or return $editor->event;
446 __PACKAGE__->register_method(
447 method => "update_hold",
448 api_name => "open-ils.circ.hold.update",
450 Updates the specified hold. The login session
451 is the requestor and if the requestor is different from the usr field
452 on the hold, the requestor must have UPDATE_HOLDS permissions.
456 my($self, $client, $login_session, $hold) = @_;
458 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
459 $login_session, $hold->usr, 'UPDATE_HOLD' );
462 $logger->activity('User ' . $requestor->id .
463 ' updating hold ' . $hold->id . ' for user ' . $target->id );
465 return $U->storagereq(
466 "open-ils.storage.direct.action.hold_request.update", $hold );
470 __PACKAGE__->register_method(
471 method => "retrieve_hold_status",
472 api_name => "open-ils.circ.hold.status.retrieve",
474 Calculates the current status of the hold.
475 the requestor must have VIEW_HOLD permissions if the hold is for a user
476 other than the requestor.
477 Returns -1 on error (for now)
478 Returns 1 for 'waiting for copy to become available'
479 Returns 2 for 'waiting for copy capture'
480 Returns 3 for 'in transit'
481 Returns 4 for 'arrived'
484 sub retrieve_hold_status {
485 my($self, $client, $auth, $hold_id) = @_;
487 my $e = new_editor(authtoken => $auth);
488 return $e->event unless $e->checkauth;
489 my $hold = $e->retrieve_action_hold_request($hold_id)
492 if( $e->requestor->id != $hold->usr ) {
493 return $e->event unless $e->allowed('VIEW_HOLD');
496 return 1 unless $hold->current_copy;
497 return 2 unless $hold->capture_time;
499 my $copy = $e->retrieve_asset_copy($hold->current_copy)
502 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
503 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
513 __PACKAGE__->register_method(
514 method => "capture_copy",
515 api_name => "open-ils.circ.hold.capture_copy.barcode",
517 Captures a copy to fulfil a hold
518 Params is login session and copy barcode
519 Optional param is 'flesh'. If set, we also return the
520 relevant copy and title
521 login mus have COPY_CHECKIN permissions (since this is essentially
525 # XXX deprecate me XXX
528 my( $self, $client, $login_session, $params ) = @_;
529 my %params = %$params;
530 my $barcode = $params{barcode};
533 my( $user, $target, $copy, $hold, $evt );
535 ( $user, $evt ) = $apputils->checkses($login_session);
538 # am I allowed to checkin a copy?
539 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
542 $logger->info("Capturing copy with barcode $barcode");
544 my $session = $apputils->start_db_session();
546 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
549 $logger->debug("Capturing copy " . $copy->id);
551 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
552 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
555 warn "Found hold " . $hold->id . "\n";
556 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
558 $hold->current_copy($copy->id);
559 $hold->capture_time("now");
562 my $stat = $session->request(
563 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
564 if(!$stat) { throw OpenSRF::EX::ERROR
565 ("Error updating hold request " . $copy->id); }
567 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
569 # if the staff member capturing this item is not at the pickup lib
570 if( $user->home_ou ne $hold->pickup_lib ) {
571 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
574 $copy->editor($user->id);
575 $copy->edit_date("now");
576 $stat = $session->request(
577 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
578 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
580 my $payload = { hold => $hold };
581 $payload->{copy} = $copy if $params{flesh_copy};
583 if($params{flesh_record}) {
585 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
587 $record = $apputils->record_to_mvr($record);
588 $payload->{record} = $record;
591 $apputils->commit_db_session($session);
593 return OpenILS::Event->new('ROUTE_ITEM',
594 route_to => $hold->pickup_lib, payload => $payload );
597 sub _build_hold_transit {
598 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
599 my $trans = Fieldmapper::action::hold_transit_copy->new;
601 $trans->hold($hold->id);
602 $trans->source($user->home_ou);
603 $trans->dest($hold->pickup_lib);
604 $trans->source_send_time("now");
605 $trans->target_copy($copy->id);
606 $trans->copy_status($copy->status);
608 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
609 my ($stat) = $meth->run( $login_session, $trans, $session );
610 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
611 else { $copy->status(6); } #status in transit
616 __PACKAGE__->register_method(
617 method => "create_hold_transit",
618 api_name => "open-ils.circ.hold_transit.create",
620 Creates a new transit object
623 sub create_hold_transit {
624 my( $self, $client, $login_session, $transit, $session ) = @_;
626 my( $user, $evt ) = $apputils->checkses($login_session);
628 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
632 if($session) { $ses = $session; }
633 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
635 return $ses->request(
636 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
642 sub find_local_hold {
643 my( $class, $session, $copy, $user ) = @_;
644 return $class->find_nearest_permitted_hold($session, $copy, $user);
652 sub fetch_open_hold_by_current_copy {
655 my $hold = $apputils->simplereq(
657 'open-ils.cstore.direct.action.hold_request.search.atomic',
658 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
659 return $hold->[0] if ref($hold);
663 sub fetch_related_holds {
666 return $apputils->simplereq(
668 'open-ils.cstore.direct.action.hold_request.search.atomic',
669 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
673 __PACKAGE__->register_method (
674 method => "hold_pull_list",
675 api_name => "open-ils.circ.hold_pull_list.retrieve",
677 Returns a list of hold ID's that need to be "pulled"
683 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
684 my( $reqr, $evt ) = $U->checkses($authtoken);
687 my $org = $reqr->ws_ou || $reqr->home_ou;
688 # the perm locaiton shouldn't really matter here since holds
689 # will exist all over and VIEW_HOLDS should be universal
690 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
693 return $U->storagereq(
694 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
695 $org, $limit, $offset );
698 __PACKAGE__->register_method (
699 method => 'fetch_hold_notify',
700 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
702 Returns a list of hold notification objects based on hold id.
703 @param authtoken The loggin session key
704 @param holdid The id of the hold whose notifications we want to retrieve
705 @return An array of hold notification objects, event on error.
709 sub fetch_hold_notify {
710 my( $self, $conn, $authtoken, $holdid ) = @_;
711 my( $requestor, $evt ) = $U->checkses($authtoken);
714 ($hold, $evt) = $U->fetch_hold($holdid);
716 ($patron, $evt) = $U->fetch_user($hold->usr);
719 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
722 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
723 return $U->cstorereq(
724 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
728 __PACKAGE__->register_method (
729 method => 'create_hold_notify',
730 api_name => 'open-ils.circ.hold_notification.create',
732 Creates a new hold notification object
733 @param authtoken The login session key
734 @param notification The hold notification object to create
735 @return ID of the new object on success, Event on error
738 sub create_hold_notify {
739 my( $self, $conn, $authtoken, $notification ) = @_;
740 my( $requestor, $evt ) = $U->checkses($authtoken);
743 ($hold, $evt) = $U->fetch_hold($notification->hold);
745 ($patron, $evt) = $U->fetch_user($hold->usr);
748 # XXX perm depth probably doesn't matter here -- should always be consortium level
749 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
752 # Set the proper notifier
753 $notification->notify_staff($requestor->id);
754 my $id = $U->storagereq(
755 'open-ils.storage.direct.action.hold_notification.create', $notification );
756 return $U->DB_UPDATE_FAILED($notification) unless $id;
757 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
762 __PACKAGE__->register_method(
763 method => 'reset_hold',
764 api_name => 'open-ils.circ.hold.reset',
766 Un-captures and un-targets a hold, essentially returning
767 it to the state it was in directly after it was placed,
768 then attempts to re-target the hold
769 @param authtoken The login session key
770 @param holdid The id of the hold
776 my( $self, $conn, $auth, $holdid ) = @_;
778 my ($hold, $evt) = $U->fetch_hold($holdid);
780 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
782 $evt = $self->_reset_hold($reqr, $hold);
788 my ($self, $reqr, $hold) = @_;
790 my $e = new_editor(xact =>1, requestor => $reqr);
792 $logger->info("reseting hold ".$hold->id);
794 if( $hold->capture_time and $hold->current_copy ) {
796 my $copy = $e->retrieve_asset_copy($hold->current_copy)
799 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
800 $logger->info("setting copy to status 'reshelving' on hold retarget");
801 $copy->status(OILS_COPY_STATUS_RESHELVING);
802 $copy->editor($e->requestor->id);
803 $copy->edit_date('now');
804 $e->update_asset_copy($copy) or return $e->event;
806 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
807 $logger->warn("reseting hold that is in transit: ".$hold->id);
812 $hold->clear_capture_time;
813 $hold->clear_current_copy;
815 $e->update_action_hold_request($hold) or return $e->event;
820 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
826 __PACKAGE__->register_method(
827 method => 'fetch_open_title_holds',
828 api_name => 'open-ils.circ.open_holds.retrieve',
830 Returns a list ids of un-fulfilled holds for a given title id
831 @param authtoken The login session key
832 @param id the id of the item whose holds we want to retrieve
833 @param type The hold type - M, T, V, C
837 sub fetch_open_title_holds {
838 my( $self, $conn, $auth, $id, $type, $org ) = @_;
839 my $e = new_editor( authtoken => $auth );
840 return $e->event unless $e->checkauth;
843 $org ||= $e->requestor->ws_ou;
845 # return $e->search_action_hold_request(
846 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
848 # XXX make me return IDs in the future ^--
849 my $holds = $e->search_action_hold_request(
852 cancel_time => undef,
854 fulfillment_time => undef
858 flesh_hold_transits($holds);
863 sub flesh_hold_transits {
865 for my $hold ( @$holds ) {
867 $apputils->simplereq(
869 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
870 { hold => $hold->id },
871 { order_by => { ahtc => 'id desc' }, limit => 1 }
880 __PACKAGE__->register_method(
881 method => 'fetch_captured_holds',
882 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
884 Returns a list ids of un-fulfilled holds for a given title id
885 @param authtoken The login session key
886 @param org The org id of the location in question
889 sub fetch_captured_holds {
890 my( $self, $conn, $auth, $org ) = @_;
892 my $e = new_editor(authtoken => $auth);
893 return $e->event unless $e->checkauth;
894 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
896 $org ||= $e->requestor->ws_ou;
898 my $holds = $e->search_action_hold_request(
900 capture_time => { "!=" => undef },
901 current_copy => { "!=" => undef },
902 fulfillment_time => undef,
904 cancel_time => undef,
909 for my $h (@$holds) {
910 my $copy = $e->retrieve_asset_copy($h->current_copy)
913 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
916 flesh_hold_transits(\@res);
924 __PACKAGE__->register_method(
925 method => "check_title_hold",
926 api_name => "open-ils.circ.title_hold.is_possible",
928 Determines if a hold were to be placed by a given user,
929 whether or not said hold would have any potential copies
931 @param authtoken The login session key
932 @param params A hash of named params including:
933 patronid - the id of the hold recipient
934 titleid (brn) - the id of the title to be held
935 depth - the hold range depth (defaults to 0)
938 sub check_title_hold {
939 my( $self, $client, $authtoken, $params ) = @_;
941 my %params = %$params;
942 my $titleid = $params{titleid} ||"";
943 my $mrid = $params{mrid} ||"";
944 my $depth = $params{depth} || 0;
945 my $pickup_lib = $params{pickup_lib};
946 my $hold_type = $params{hold_type} || 'T';
948 my $e = new_editor(authtoken=>$authtoken);
949 return $e->event unless $e->checkauth;
950 my $patron = $e->retrieve_actor_user($params{patronid})
953 if( $e->requestor->id ne $patron->id ) {
954 return $e->event unless
955 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
958 return OpenILS::Event->new('PATRON_BARRED')
959 if $patron->barred and
960 ($patron->barred =~ /t/i or $patron->barred == 1);
962 my $rangelib = $params{range_lib} || $patron->home_ou;
964 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
967 if( $hold_type eq 'T' ) {
968 return _check_title_hold_is_possible(
969 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
972 if( $hold_type eq 'M' ) {
973 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
974 my @recs = map { $_->source } @$maps;
975 for my $rec (@recs) {
976 return 1 if (_check_title_hold_is_possible(
977 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
984 sub _check_title_hold_is_possible {
985 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
991 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
993 while( $title = $U->storagereq(
994 'open-ils.storage.biblio.record_entry.ranged_tree',
995 $titleid, $rangelib, $depth, $limit, $offset ) ) {
999 ref($title->call_numbers) and
1000 @{$title->call_numbers};
1002 for my $cn (@{$title->call_numbers}) {
1004 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1006 for my $copy (@{$cn->copies}) {
1008 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1010 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1011 { patron => $patron,
1012 requestor => $requestor,
1015 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1016 pickup_lib => $pickup_lib,
1017 request_lib => $request_lib
1021 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1032 sub find_nearest_permitted_hold {
1035 my $session = shift;
1038 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1040 # first see if this copy has already been selected to fulfill a hold
1041 my $hold = $session->request(
1042 "open-ils.storage.direct.action.hold_request.search_where",
1043 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1046 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1050 # We know this hold is permitted, so just return it
1051 return $hold if $hold;
1053 $logger->debug("searching for potential holds at org ".
1054 $user->ws_ou." and copy ".$copy->id);
1056 my $holds = $session->request(
1057 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1058 $user->ws_ou, $copy->id, 5 )->gather(1);
1060 return (undef, $evt) unless @$holds;
1062 # for each potential hold, we have to run the permit script
1063 # to make sure the hold is actually permitted.
1065 for my $holdid (@$holds) {
1066 next unless $holdid;
1067 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1069 my ($hold) = $U->fetch_hold($holdid);
1071 my ($reqr) = $U->fetch_user($hold->requestor);
1073 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1075 patron_id => $hold->usr,
1076 requestor => $reqr->id,
1078 pickup_lib => $hold->pickup_lib,
1079 request_lib => $hold->request_lib
1084 return (undef, $evt);
1088 __PACKAGE__->register_method(
1089 method => 'all_rec_holds',
1090 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1094 my( $self, $conn, $auth, $title_id, $args ) = @_;
1096 my $e = new_editor(authtoken=>$auth);
1097 $e->checkauth or return $e->event;
1098 $e->allowed('VIEW_HOLD') or return $e->event;
1100 $args ||= { fulfillment_time => undef };
1101 $args->{cancel_time} = undef;
1105 $resp->{title_holds} = $e->search_action_hold_request(
1107 hold_type => OILS_HOLD_TYPE_TITLE,
1108 target => $title_id,
1112 my $vols = $e->search_asset_call_number(
1113 { record => $title_id, deleted => 'f' }, {idlist=>1});
1115 $resp->{volume_holds} = (!@$vols) ? [] : $e->search_action_hold_request(
1117 hold_type => OILS_HOLD_TYPE_VOLUME,
1122 my $copies = $e->search_asset_copy(
1123 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1125 $resp->{copy_holds} = (!@$copies) ? [] : $e->search_action_hold_request(
1127 hold_type => OILS_HOLD_TYPE_COPY,