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, $auth, $hold_id) = @_;
289 my $e = new_editor(authtoken=>$auth);
290 $e->checkauth or return $e->event;
291 $e->allowed('VIEW_HOLD') or return $e->event;
293 my $holds = $e->search_action_hold_request(
295 { id => $hold_id , fulfillment_time => undef },
296 { order_by => { ahr => "request_time" } }
300 flesh_hold_transits($holds);
301 flesh_hold_notices($holds, $e);
306 __PACKAGE__->register_method(
307 method => "retrieve_holds",
308 api_name => "open-ils.circ.holds.retrieve",
310 Retrieves all the holds, with hold transits attached, for the specified
311 user id. The login session is the requestor and if the requestor is
312 different from the user, then the requestor must have VIEW_HOLD permissions.
315 __PACKAGE__->register_method(
316 method => "retrieve_holds",
317 api_name => "open-ils.circ.holds.id_list.retrieve",
319 Retrieves all the hold ids for the specified
320 user id. The login session is the requestor and if the requestor is
321 different from the user, then the requestor must have VIEW_HOLD permissions.
325 my($self, $client, $login_session, $user_id) = @_;
327 my( $user, $target, $evt ) = $apputils->checkses_requestor(
328 $login_session, $user_id, 'VIEW_HOLD' );
331 my $holds = $apputils->simplereq(
333 "open-ils.cstore.direct.action.hold_request.search.atomic",
336 fulfillment_time => undef,
337 cancel_time => undef,
339 { order_by => { ahr => "request_time" } }
342 if( ! $self->api_name =~ /id_list/ ) {
343 for my $hold ( @$holds ) {
345 $apputils->simplereq(
347 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
348 { hold => $hold->id },
349 { order_by => { ahtc => 'id desc' }, limit => 1 }
355 if( $self->api_name =~ /id_list/ ) {
356 return [ map { $_->id } @$holds ];
362 __PACKAGE__->register_method(
363 method => "retrieve_holds_by_pickup_lib",
364 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
366 Retrieves all the holds, with hold transits attached, for the specified
370 __PACKAGE__->register_method(
371 method => "retrieve_holds_by_pickup_lib",
372 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
374 Retrieves all the hold ids for the specified
378 sub retrieve_holds_by_pickup_lib {
379 my($self, $client, $login_session, $ou_id) = @_;
381 #FIXME -- put an appropriate permission check here
382 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
383 # $login_session, $user_id, 'VIEW_HOLD' );
384 #return $evt if $evt;
386 my $holds = $apputils->simplereq(
388 "open-ils.cstore.direct.action.hold_request.search.atomic",
390 pickup_lib => $ou_id ,
391 fulfillment_time => undef,
394 { order_by => { ahr => "request_time" } });
397 if( ! $self->api_name =~ /id_list/ ) {
398 flesh_hold_transits($holds);
401 if( $self->api_name =~ /id_list/ ) {
402 return [ map { $_->id } @$holds ];
408 __PACKAGE__->register_method(
409 method => "cancel_hold",
410 api_name => "open-ils.circ.hold.cancel",
412 Cancels the specified hold. The login session
413 is the requestor and if the requestor is different from the usr field
414 on the hold, the requestor must have CANCEL_HOLDS permissions.
415 the hold may be either the hold object or the hold id
419 my($self, $client, $auth, $holdid) = @_;
421 my $e = new_editor(authtoken=>$auth, xact=>1);
422 return $e->event unless $e->checkauth;
424 my $hold = $e->retrieve_action_hold_request($holdid)
427 if( $e->requestor->id ne $hold->usr ) {
428 return $e->event unless $e->allowed('CANCEL_HOLDS');
431 return 1 if $hold->cancel_time;
433 # If the hold is captured, reset the copy status
434 if( $hold->capture_time and $hold->current_copy ) {
436 my $copy = $e->retrieve_asset_copy($hold->current_copy)
439 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
440 $logger->info("setting copy to status 'reshelving' on hold cancel");
441 $copy->status(OILS_COPY_STATUS_RESHELVING);
442 $copy->editor($e->requestor->id);
443 $copy->edit_date('now');
444 $e->update_asset_copy($copy) or return $e->event;
448 $hold->cancel_time('now');
449 $e->update_action_hold_request($hold)
452 $self->delete_hold_copy_maps($e, $hold->id);
458 sub delete_hold_copy_maps {
463 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
465 $editor->delete_action_hold_copy_map($_)
466 or return $editor->event;
472 __PACKAGE__->register_method(
473 method => "update_hold",
474 api_name => "open-ils.circ.hold.update",
476 Updates the specified hold. The login session
477 is the requestor and if the requestor is different from the usr field
478 on the hold, the requestor must have UPDATE_HOLDS permissions.
482 my($self, $client, $login_session, $hold) = @_;
484 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
485 $login_session, $hold->usr, 'UPDATE_HOLD' );
488 $logger->activity('User ' . $requestor->id .
489 ' updating hold ' . $hold->id . ' for user ' . $target->id );
491 return $U->storagereq(
492 "open-ils.storage.direct.action.hold_request.update", $hold );
496 __PACKAGE__->register_method(
497 method => "retrieve_hold_status",
498 api_name => "open-ils.circ.hold.status.retrieve",
500 Calculates the current status of the hold.
501 the requestor must have VIEW_HOLD permissions if the hold is for a user
502 other than the requestor.
503 Returns -1 on error (for now)
504 Returns 1 for 'waiting for copy to become available'
505 Returns 2 for 'waiting for copy capture'
506 Returns 3 for 'in transit'
507 Returns 4 for 'arrived'
510 sub retrieve_hold_status {
511 my($self, $client, $auth, $hold_id) = @_;
513 my $e = new_editor(authtoken => $auth);
514 return $e->event unless $e->checkauth;
515 my $hold = $e->retrieve_action_hold_request($hold_id)
518 if( $e->requestor->id != $hold->usr ) {
519 return $e->event unless $e->allowed('VIEW_HOLD');
522 return _hold_status($e, $hold);
528 return 1 unless $hold->current_copy;
529 return 2 unless $hold->capture_time;
531 my $copy = $e->retrieve_asset_copy($hold->current_copy)
534 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
535 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
545 __PACKAGE__->register_method(
546 method => "capture_copy",
547 api_name => "open-ils.circ.hold.capture_copy.barcode",
549 Captures a copy to fulfil a hold
550 Params is login session and copy barcode
551 Optional param is 'flesh'. If set, we also return the
552 relevant copy and title
553 login mus have COPY_CHECKIN permissions (since this is essentially
557 # XXX deprecate me XXX
560 my( $self, $client, $login_session, $params ) = @_;
561 my %params = %$params;
562 my $barcode = $params{barcode};
565 my( $user, $target, $copy, $hold, $evt );
567 ( $user, $evt ) = $apputils->checkses($login_session);
570 # am I allowed to checkin a copy?
571 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
574 $logger->info("Capturing copy with barcode $barcode");
576 my $session = $apputils->start_db_session();
578 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
581 $logger->debug("Capturing copy " . $copy->id);
583 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
584 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
587 warn "Found hold " . $hold->id . "\n";
588 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
590 $hold->current_copy($copy->id);
591 $hold->capture_time("now");
594 my $stat = $session->request(
595 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
596 if(!$stat) { throw OpenSRF::EX::ERROR
597 ("Error updating hold request " . $copy->id); }
599 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
601 # if the staff member capturing this item is not at the pickup lib
602 if( $user->home_ou ne $hold->pickup_lib ) {
603 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
606 $copy->editor($user->id);
607 $copy->edit_date("now");
608 $stat = $session->request(
609 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
610 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
612 my $payload = { hold => $hold };
613 $payload->{copy} = $copy if $params{flesh_copy};
615 if($params{flesh_record}) {
617 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
619 $record = $apputils->record_to_mvr($record);
620 $payload->{record} = $record;
623 $apputils->commit_db_session($session);
625 return OpenILS::Event->new('ROUTE_ITEM',
626 route_to => $hold->pickup_lib, payload => $payload );
629 sub _build_hold_transit {
630 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
631 my $trans = Fieldmapper::action::hold_transit_copy->new;
633 $trans->hold($hold->id);
634 $trans->source($user->home_ou);
635 $trans->dest($hold->pickup_lib);
636 $trans->source_send_time("now");
637 $trans->target_copy($copy->id);
638 $trans->copy_status($copy->status);
640 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
641 my ($stat) = $meth->run( $login_session, $trans, $session );
642 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
643 else { $copy->status(6); } #status in transit
648 __PACKAGE__->register_method(
649 method => "create_hold_transit",
650 api_name => "open-ils.circ.hold_transit.create",
652 Creates a new transit object
655 sub create_hold_transit {
656 my( $self, $client, $login_session, $transit, $session ) = @_;
658 my( $user, $evt ) = $apputils->checkses($login_session);
660 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
664 if($session) { $ses = $session; }
665 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
667 return $ses->request(
668 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
674 sub find_local_hold {
675 my( $class, $session, $copy, $user ) = @_;
676 return $class->find_nearest_permitted_hold($session, $copy, $user);
684 sub fetch_open_hold_by_current_copy {
687 my $hold = $apputils->simplereq(
689 'open-ils.cstore.direct.action.hold_request.search.atomic',
690 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
691 return $hold->[0] if ref($hold);
695 sub fetch_related_holds {
698 return $apputils->simplereq(
700 'open-ils.cstore.direct.action.hold_request.search.atomic',
701 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
705 __PACKAGE__->register_method (
706 method => "hold_pull_list",
707 api_name => "open-ils.circ.hold_pull_list.retrieve",
709 Returns a list of holds that need to be "pulled"
714 __PACKAGE__->register_method (
715 method => "hold_pull_list",
716 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
718 Returns a list of hold ID's that need to be "pulled"
725 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
726 my( $reqr, $evt ) = $U->checkses($authtoken);
729 my $org = $reqr->ws_ou || $reqr->home_ou;
730 # the perm locaiton shouldn't really matter here since holds
731 # will exist all over and VIEW_HOLDS should be universal
732 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
735 if( $self->api_name =~ /id_list/ ) {
736 return $U->storagereq(
737 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
738 $org, $limit, $offset );
740 return $U->storagereq(
741 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
742 $org, $limit, $offset );
746 __PACKAGE__->register_method (
747 method => 'fetch_hold_notify',
748 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
750 Returns a list of hold notification objects based on hold id.
751 @param authtoken The loggin session key
752 @param holdid The id of the hold whose notifications we want to retrieve
753 @return An array of hold notification objects, event on error.
757 sub fetch_hold_notify {
758 my( $self, $conn, $authtoken, $holdid ) = @_;
759 my( $requestor, $evt ) = $U->checkses($authtoken);
762 ($hold, $evt) = $U->fetch_hold($holdid);
764 ($patron, $evt) = $U->fetch_user($hold->usr);
767 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
770 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
771 return $U->cstorereq(
772 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
776 __PACKAGE__->register_method (
777 method => 'create_hold_notify',
778 api_name => 'open-ils.circ.hold_notification.create',
780 Creates a new hold notification object
781 @param authtoken The login session key
782 @param notification The hold notification object to create
783 @return ID of the new object on success, Event on error
786 sub create_hold_notify {
787 my( $self, $conn, $authtoken, $notification ) = @_;
788 my( $requestor, $evt ) = $U->checkses($authtoken);
791 ($hold, $evt) = $U->fetch_hold($notification->hold);
793 ($patron, $evt) = $U->fetch_user($hold->usr);
796 # XXX perm depth probably doesn't matter here -- should always be consortium level
797 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
800 # Set the proper notifier
801 $notification->notify_staff($requestor->id);
802 my $id = $U->storagereq(
803 'open-ils.storage.direct.action.hold_notification.create', $notification );
804 return $U->DB_UPDATE_FAILED($notification) unless $id;
805 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
810 __PACKAGE__->register_method(
811 method => 'reset_hold',
812 api_name => 'open-ils.circ.hold.reset',
814 Un-captures and un-targets a hold, essentially returning
815 it to the state it was in directly after it was placed,
816 then attempts to re-target the hold
817 @param authtoken The login session key
818 @param holdid The id of the hold
824 my( $self, $conn, $auth, $holdid ) = @_;
826 my ($hold, $evt) = $U->fetch_hold($holdid);
828 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
830 $evt = $self->_reset_hold($reqr, $hold);
836 my ($self, $reqr, $hold) = @_;
838 my $e = new_editor(xact =>1, requestor => $reqr);
840 $logger->info("reseting hold ".$hold->id);
842 if( $hold->capture_time and $hold->current_copy ) {
844 my $copy = $e->retrieve_asset_copy($hold->current_copy)
847 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
848 $logger->info("setting copy to status 'reshelving' on hold retarget");
849 $copy->status(OILS_COPY_STATUS_RESHELVING);
850 $copy->editor($e->requestor->id);
851 $copy->edit_date('now');
852 $e->update_asset_copy($copy) or return $e->event;
854 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
855 $logger->warn("reseting hold that is in transit: ".$hold->id);
860 $hold->clear_capture_time;
861 $hold->clear_current_copy;
863 $e->update_action_hold_request($hold) or return $e->event;
868 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
874 __PACKAGE__->register_method(
875 method => 'fetch_open_title_holds',
876 api_name => 'open-ils.circ.open_holds.retrieve',
878 Returns a list ids of un-fulfilled holds for a given title id
879 @param authtoken The login session key
880 @param id the id of the item whose holds we want to retrieve
881 @param type The hold type - M, T, V, C
885 sub fetch_open_title_holds {
886 my( $self, $conn, $auth, $id, $type, $org ) = @_;
887 my $e = new_editor( authtoken => $auth );
888 return $e->event unless $e->checkauth;
891 $org ||= $e->requestor->ws_ou;
893 # return $e->search_action_hold_request(
894 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
896 # XXX make me return IDs in the future ^--
897 my $holds = $e->search_action_hold_request(
900 cancel_time => undef,
902 fulfillment_time => undef
906 flesh_hold_transits($holds);
911 sub flesh_hold_transits {
913 for my $hold ( @$holds ) {
915 $apputils->simplereq(
917 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
918 { hold => $hold->id },
919 { order_by => { ahtc => 'id desc' }, limit => 1 }
925 sub flesh_hold_notices {
926 my( $holds, $e ) = @_;
929 for my $hold (@$holds) {
930 my $notices = $e->search_action_hold_notification(
932 { hold => $hold->id },
933 { order_by => { anh => { 'notify_time desc' } } },
938 $hold->notify_count(scalar(@$notices));
940 my $n = $e->retrieve_action_hold_notification($$notices[0])
942 $hold->notify_time($n->notify_time);
950 __PACKAGE__->register_method(
951 method => 'fetch_captured_holds',
952 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
954 Returns a list of un-fulfilled holds for a given title id
955 @param authtoken The login session key
956 @param org The org id of the location in question
960 __PACKAGE__->register_method(
961 method => 'fetch_captured_holds',
962 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
964 Returns a list ids of un-fulfilled holds for a given title id
965 @param authtoken The login session key
966 @param org The org id of the location in question
970 sub fetch_captured_holds {
971 my( $self, $conn, $auth, $org ) = @_;
973 my $e = new_editor(authtoken => $auth);
974 return $e->event unless $e->checkauth;
975 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
977 $org ||= $e->requestor->ws_ou;
979 my $holds = $e->search_action_hold_request(
981 capture_time => { "!=" => undef },
982 current_copy => { "!=" => undef },
983 fulfillment_time => undef,
985 cancel_time => undef,
990 for my $h (@$holds) {
991 my $copy = $e->retrieve_asset_copy($h->current_copy)
994 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
997 if( ! $self->api_name =~ /id_list/ ) {
998 flesh_hold_transits(\@res);
999 flesh_hold_notices(\@res, $e);
1002 if( $self->api_name =~ /id_list/ ) {
1003 return [ map { $_->id } @res ];
1010 __PACKAGE__->register_method(
1011 method => "check_title_hold",
1012 api_name => "open-ils.circ.title_hold.is_possible",
1014 Determines if a hold were to be placed by a given user,
1015 whether or not said hold would have any potential copies
1017 @param authtoken The login session key
1018 @param params A hash of named params including:
1019 patronid - the id of the hold recipient
1020 titleid (brn) - the id of the title to be held
1021 depth - the hold range depth (defaults to 0)
1024 sub check_title_hold {
1025 my( $self, $client, $authtoken, $params ) = @_;
1027 my %params = %$params;
1028 my $titleid = $params{titleid} ||"";
1029 my $mrid = $params{mrid} ||"";
1030 my $depth = $params{depth} || 0;
1031 my $pickup_lib = $params{pickup_lib};
1032 my $hold_type = $params{hold_type} || 'T';
1034 my $e = new_editor(authtoken=>$authtoken);
1035 return $e->event unless $e->checkauth;
1036 my $patron = $e->retrieve_actor_user($params{patronid})
1037 or return $e->event;
1039 if( $e->requestor->id ne $patron->id ) {
1040 return $e->event unless
1041 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1044 return OpenILS::Event->new('PATRON_BARRED')
1045 if $patron->barred and
1046 ($patron->barred =~ /t/i or $patron->barred == 1);
1048 my $rangelib = $params{range_lib} || $patron->home_ou;
1050 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1051 or return $e->event;
1053 if( $hold_type eq 'T' ) {
1054 return _check_title_hold_is_possible(
1055 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1058 if( $hold_type eq 'M' ) {
1059 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1060 my @recs = map { $_->source } @$maps;
1061 for my $rec (@recs) {
1062 return 1 if (_check_title_hold_is_possible(
1063 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1070 sub _check_title_hold_is_possible {
1071 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1077 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1079 while( $title = $U->storagereq(
1080 'open-ils.storage.biblio.record_entry.ranged_tree',
1081 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1085 ref($title->call_numbers) and
1086 @{$title->call_numbers};
1088 for my $cn (@{$title->call_numbers}) {
1090 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1092 for my $copy (@{$cn->copies}) {
1094 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1096 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1097 { patron => $patron,
1098 requestor => $requestor,
1101 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1102 pickup_lib => $pickup_lib,
1103 request_lib => $request_lib
1107 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1118 sub find_nearest_permitted_hold {
1121 my $session = shift;
1124 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1126 # first see if this copy has already been selected to fulfill a hold
1127 my $hold = $session->request(
1128 "open-ils.storage.direct.action.hold_request.search_where",
1129 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1132 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1136 # We know this hold is permitted, so just return it
1137 return $hold if $hold;
1139 $logger->debug("searching for potential holds at org ".
1140 $user->ws_ou." and copy ".$copy->id);
1142 my $holds = $session->request(
1143 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1144 $user->ws_ou, $copy->id, 5 )->gather(1);
1146 return (undef, $evt) unless @$holds;
1148 # for each potential hold, we have to run the permit script
1149 # to make sure the hold is actually permitted.
1151 for my $holdid (@$holds) {
1152 next unless $holdid;
1153 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1155 my ($hold) = $U->fetch_hold($holdid);
1157 my ($reqr) = $U->fetch_user($hold->requestor);
1159 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1161 patron_id => $hold->usr,
1162 requestor => $reqr->id,
1164 pickup_lib => $hold->pickup_lib,
1165 request_lib => $hold->request_lib
1170 return (undef, $evt);
1174 __PACKAGE__->register_method(
1175 method => 'all_rec_holds',
1176 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1180 my( $self, $conn, $auth, $title_id, $args ) = @_;
1182 my $e = new_editor(authtoken=>$auth);
1183 $e->checkauth or return $e->event;
1184 $e->allowed('VIEW_HOLD') or return $e->event;
1186 $args ||= { fulfillment_time => undef };
1187 $args->{cancel_time} = undef;
1189 my $resp = { volume_holds => [], copy_holds => [] };
1191 $resp->{title_holds} = $e->search_action_hold_request(
1193 hold_type => OILS_HOLD_TYPE_TITLE,
1194 target => $title_id,
1198 my $vols = $e->search_asset_call_number(
1199 { record => $title_id, deleted => 'f' }, {idlist=>1});
1201 return $resp unless @$vols;
1203 $resp->{volume_holds} = $e->search_action_hold_request(
1205 hold_type => OILS_HOLD_TYPE_VOLUME,
1210 my $copies = $e->search_asset_copy(
1211 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1213 return $resp unless @$copies;
1215 $resp->{copy_holds} = $e->search_action_hold_request(
1217 hold_type => OILS_HOLD_TYPE_COPY,
1227 __PACKAGE__->register_method(
1228 method => 'uber_hold',
1229 api_name => 'open-ils.circ.hold.details.retrieve'
1233 my($self, $client, $auth, $hold_id) = @_;
1234 my $e = new_editor(authtoken=>$auth);
1235 $e->checkauth or return $e->event;
1236 $e->allowed('VIEW_HOLD') or return $e->event;
1240 my $hold = $e->retrieve_action_hold_request(
1245 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1248 ) or return $e->event;
1250 my $user = $hold->usr;
1251 $hold->usr($user->id);
1253 my $card = $e->retrieve_actor_card($user->card)
1254 or return $e->event;
1256 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1258 flesh_hold_notices([$hold], $e);
1259 flesh_hold_transits([$hold]);
1266 status => _hold_status($e, $hold),
1267 patron_first => $user->first_given_name,
1268 patron_last => $user->family_name,
1269 patron_barcode => $card->barcode,
1275 # -----------------------------------------------------
1276 # Returns the MVR object that represents what the
1278 # -----------------------------------------------------
1280 my( $e, $hold ) = @_;
1286 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1287 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1288 or return $e->event;
1289 $tid = $mr->master_record;
1291 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1292 $tid = $hold->target;
1294 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1295 $volume = $e->retrieve_asset_call_number($hold->target)
1296 or return $e->event;
1297 $tid = $volume->record;
1299 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1300 $copy = $e->retrieve_asset_copy($hold->target)
1301 or return $e->event;
1302 $volume = $e->retrieve_asset_call_number($copy->call_number)
1303 or return $e->event;
1304 $tid = $volume->record;
1307 if(!$copy and ref $hold->current_copy ) {
1308 $copy = $hold->current_copy;
1309 $hold->current_copy($copy->id);
1312 if(!$volume and $copy) {
1313 $volume = $e->retrieve_asset_call_number($copy->call_number);
1316 my $title = $e->retrieve_biblio_record_entry($tid);
1317 return ( $U->record_to_mvr($title), $volume, $copy );