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 # Go ahead and target the copy-level holds
152 'open-ils.storage.action.hold_request.copy_targeter',
153 undef, $_->id ) for @copyholds;
159 my( $self, $client, $login_session, @holds) = @_;
161 if(!@holds){return 0;}
162 my( $user, $evt ) = $apputils->checkses($login_session);
166 if(ref($holds[0]) eq 'ARRAY') {
168 } else { $holds = [ @holds ]; }
170 $logger->debug("Iterating over holds requests...");
172 for my $hold (@$holds) {
175 my $type = $hold->hold_type;
177 $logger->activity("User " . $user->id .
178 " creating new hold of type $type for user " . $hold->usr);
181 if($user->id ne $hold->usr) {
182 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
192 # am I allowed to place holds for this user?
193 if($hold->requestor ne $hold->usr) {
194 $perm = _check_request_holds_perm($user->id, $user->home_ou);
195 if($perm) { return $perm; }
198 # is this user allowed to have holds of this type?
199 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
201 #if there is a requestor, see if the requestor has override privelages
202 if($hold->requestor ne $hold->usr) {
203 $perm = _check_request_holds_override($user->id, $user->home_ou);
204 if($perm) {return $perm;}
211 #enforce the fact that the login is the one requesting the hold
212 $hold->requestor($user->id);
213 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
215 my $resp = $apputils->simplereq(
217 'open-ils.storage.direct.action.hold_request.create', $hold );
220 return OpenSRF::EX::ERROR ("Error creating hold");
227 # makes sure that a user has permission to place the type of requested hold
228 # returns the Perm exception if not allowed, returns undef if all is well
229 sub _check_holds_perm {
230 my($type, $user_id, $org_id) = @_;
234 if($evt = $apputils->check_perms(
235 $user_id, $org_id, "MR_HOLDS")) {
239 } elsif ($type eq "T") {
240 if($evt = $apputils->check_perms(
241 $user_id, $org_id, "TITLE_HOLDS")) {
245 } elsif($type eq "V") {
246 if($evt = $apputils->check_perms(
247 $user_id, $org_id, "VOLUME_HOLDS")) {
251 } elsif($type eq "C") {
252 if($evt = $apputils->check_perms(
253 $user_id, $org_id, "COPY_HOLDS")) {
261 # tests if the given user is allowed to place holds on another's behalf
262 sub _check_request_holds_perm {
265 if(my $evt = $apputils->check_perms(
266 $user_id, $org_id, "REQUEST_HOLDS")) {
271 sub _check_request_holds_override {
274 if(my $evt = $apputils->check_perms(
275 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
280 __PACKAGE__->register_method(
281 method => "retrieve_holds_by_id",
282 api_name => "open-ils.circ.holds.retrieve_by_id",
284 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
285 different from the user, then the requestor must have VIEW_HOLD permissions.
289 sub retrieve_holds_by_id {
290 my($self, $client, $auth, $hold_id) = @_;
291 my $e = new_editor(authtoken=>$auth);
292 $e->checkauth or return $e->event;
293 $e->allowed('VIEW_HOLD') or return $e->event;
295 my $holds = $e->search_action_hold_request(
297 { id => $hold_id , fulfillment_time => undef },
298 { order_by => { ahr => "request_time" } }
302 flesh_hold_transits($holds);
303 flesh_hold_notices($holds, $e);
308 __PACKAGE__->register_method(
309 method => "retrieve_holds",
310 api_name => "open-ils.circ.holds.retrieve",
312 Retrieves all the holds, with hold transits attached, for the specified
313 user id. The login session is the requestor and if the requestor is
314 different from the user, then the requestor must have VIEW_HOLD permissions.
317 __PACKAGE__->register_method(
318 method => "retrieve_holds",
319 api_name => "open-ils.circ.holds.id_list.retrieve",
321 Retrieves all the hold ids for the specified
322 user id. The login session is the requestor and if the requestor is
323 different from the user, then the requestor must have VIEW_HOLD permissions.
327 my($self, $client, $login_session, $user_id) = @_;
329 my( $user, $target, $evt ) = $apputils->checkses_requestor(
330 $login_session, $user_id, 'VIEW_HOLD' );
333 my $holds = $apputils->simplereq(
335 "open-ils.cstore.direct.action.hold_request.search.atomic",
338 fulfillment_time => undef,
339 cancel_time => undef,
341 { order_by => { ahr => "request_time" } }
344 if( ! $self->api_name =~ /id_list/ ) {
345 for my $hold ( @$holds ) {
347 $apputils->simplereq(
349 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
350 { hold => $hold->id },
351 { order_by => { ahtc => 'id desc' }, limit => 1 }
357 if( $self->api_name =~ /id_list/ ) {
358 return [ map { $_->id } @$holds ];
364 __PACKAGE__->register_method(
365 method => "retrieve_holds_by_pickup_lib",
366 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
368 Retrieves all the holds, with hold transits attached, for the specified
372 __PACKAGE__->register_method(
373 method => "retrieve_holds_by_pickup_lib",
374 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
376 Retrieves all the hold ids for the specified
380 sub retrieve_holds_by_pickup_lib {
381 my($self, $client, $login_session, $ou_id) = @_;
383 #FIXME -- put an appropriate permission check here
384 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
385 # $login_session, $user_id, 'VIEW_HOLD' );
386 #return $evt if $evt;
388 my $holds = $apputils->simplereq(
390 "open-ils.cstore.direct.action.hold_request.search.atomic",
392 pickup_lib => $ou_id ,
393 fulfillment_time => undef,
396 { order_by => { ahr => "request_time" } });
399 if( ! $self->api_name =~ /id_list/ ) {
400 flesh_hold_transits($holds);
403 if( $self->api_name =~ /id_list/ ) {
404 return [ map { $_->id } @$holds ];
410 __PACKAGE__->register_method(
411 method => "cancel_hold",
412 api_name => "open-ils.circ.hold.cancel",
414 Cancels the specified hold. The login session
415 is the requestor and if the requestor is different from the usr field
416 on the hold, the requestor must have CANCEL_HOLDS permissions.
417 the hold may be either the hold object or the hold id
421 my($self, $client, $auth, $holdid) = @_;
423 my $e = new_editor(authtoken=>$auth, xact=>1);
424 return $e->event unless $e->checkauth;
426 my $hold = $e->retrieve_action_hold_request($holdid)
429 if( $e->requestor->id ne $hold->usr ) {
430 return $e->event unless $e->allowed('CANCEL_HOLDS');
433 return 1 if $hold->cancel_time;
435 # If the hold is captured, reset the copy status
436 if( $hold->capture_time and $hold->current_copy ) {
438 my $copy = $e->retrieve_asset_copy($hold->current_copy)
441 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
442 $logger->info("setting copy to status 'reshelving' on hold cancel");
443 $copy->status(OILS_COPY_STATUS_RESHELVING);
444 $copy->editor($e->requestor->id);
445 $copy->edit_date('now');
446 $e->update_asset_copy($copy) or return $e->event;
448 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
451 $logger->warn("! canceling hold [$hid] that is in transit");
452 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
455 my $trans = $e->retrieve_action_transit_copy($transid);
457 $logger->info("Aborting transit [$transid] on hold [$hid] cancel...");
458 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
459 $logger->info("Transit abort completed with result $evt");
460 return $evt unless "$evt" eq 1;
464 # We don't want the copy to remain "in transit" or to recover
465 # any previous statuses
466 $logger->info("setting copy back to reshelving in hold+transit cancel");
467 $copy->status(OILS_COPY_STATUS_RESHELVING);
468 $copy->editor($e->requestor->id);
469 $copy->edit_date('now');
470 $e->update_asset_copy($copy) or return $e->event;
474 $hold->cancel_time('now');
475 $e->update_action_hold_request($hold)
478 $self->delete_hold_copy_maps($e, $hold->id);
484 sub delete_hold_copy_maps {
489 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
491 $editor->delete_action_hold_copy_map($_)
492 or return $editor->event;
498 __PACKAGE__->register_method(
499 method => "update_hold",
500 api_name => "open-ils.circ.hold.update",
502 Updates the specified hold. The login session
503 is the requestor and if the requestor is different from the usr field
504 on the hold, the requestor must have UPDATE_HOLDS permissions.
508 my($self, $client, $login_session, $hold) = @_;
510 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
511 $login_session, $hold->usr, 'UPDATE_HOLD' );
514 $logger->activity('User ' . $requestor->id .
515 ' updating hold ' . $hold->id . ' for user ' . $target->id );
517 return $U->storagereq(
518 "open-ils.storage.direct.action.hold_request.update", $hold );
522 __PACKAGE__->register_method(
523 method => "retrieve_hold_status",
524 api_name => "open-ils.circ.hold.status.retrieve",
526 Calculates the current status of the hold.
527 the requestor must have VIEW_HOLD permissions if the hold is for a user
528 other than the requestor.
529 Returns -1 on error (for now)
530 Returns 1 for 'waiting for copy to become available'
531 Returns 2 for 'waiting for copy capture'
532 Returns 3 for 'in transit'
533 Returns 4 for 'arrived'
536 sub retrieve_hold_status {
537 my($self, $client, $auth, $hold_id) = @_;
539 my $e = new_editor(authtoken => $auth);
540 return $e->event unless $e->checkauth;
541 my $hold = $e->retrieve_action_hold_request($hold_id)
544 if( $e->requestor->id != $hold->usr ) {
545 return $e->event unless $e->allowed('VIEW_HOLD');
548 return _hold_status($e, $hold);
554 return 1 unless $hold->current_copy;
555 return 2 unless $hold->capture_time;
557 my $copy = $e->retrieve_asset_copy($hold->current_copy)
560 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
561 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
571 __PACKAGE__->register_method(
572 method => "capture_copy",
573 api_name => "open-ils.circ.hold.capture_copy.barcode",
575 Captures a copy to fulfil a hold
576 Params is login session and copy barcode
577 Optional param is 'flesh'. If set, we also return the
578 relevant copy and title
579 login mus have COPY_CHECKIN permissions (since this is essentially
583 # XXX deprecate me XXX
586 my( $self, $client, $login_session, $params ) = @_;
587 my %params = %$params;
588 my $barcode = $params{barcode};
591 my( $user, $target, $copy, $hold, $evt );
593 ( $user, $evt ) = $apputils->checkses($login_session);
596 # am I allowed to checkin a copy?
597 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
600 $logger->info("Capturing copy with barcode $barcode");
602 my $session = $apputils->start_db_session();
604 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
607 $logger->debug("Capturing copy " . $copy->id);
609 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
610 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
613 warn "Found hold " . $hold->id . "\n";
614 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
616 $hold->current_copy($copy->id);
617 $hold->capture_time("now");
620 my $stat = $session->request(
621 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
622 if(!$stat) { throw OpenSRF::EX::ERROR
623 ("Error updating hold request " . $copy->id); }
625 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
627 # if the staff member capturing this item is not at the pickup lib
628 if( $user->home_ou ne $hold->pickup_lib ) {
629 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
632 $copy->editor($user->id);
633 $copy->edit_date("now");
634 $stat = $session->request(
635 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
636 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
638 my $payload = { hold => $hold };
639 $payload->{copy} = $copy if $params{flesh_copy};
641 if($params{flesh_record}) {
643 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
645 $record = $apputils->record_to_mvr($record);
646 $payload->{record} = $record;
649 $apputils->commit_db_session($session);
651 return OpenILS::Event->new('ROUTE_ITEM',
652 route_to => $hold->pickup_lib, payload => $payload );
655 sub _build_hold_transit {
656 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
657 my $trans = Fieldmapper::action::hold_transit_copy->new;
659 $trans->hold($hold->id);
660 $trans->source($user->home_ou);
661 $trans->dest($hold->pickup_lib);
662 $trans->source_send_time("now");
663 $trans->target_copy($copy->id);
664 $trans->copy_status($copy->status);
666 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
667 my ($stat) = $meth->run( $login_session, $trans, $session );
668 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
669 else { $copy->status(6); } #status in transit
674 __PACKAGE__->register_method(
675 method => "create_hold_transit",
676 api_name => "open-ils.circ.hold_transit.create",
678 Creates a new transit object
681 sub create_hold_transit {
682 my( $self, $client, $login_session, $transit, $session ) = @_;
684 my( $user, $evt ) = $apputils->checkses($login_session);
686 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
690 if($session) { $ses = $session; }
691 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
693 return $ses->request(
694 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
700 sub find_local_hold {
701 my( $class, $session, $copy, $user ) = @_;
702 return $class->find_nearest_permitted_hold($session, $copy, $user);
710 sub fetch_open_hold_by_current_copy {
713 my $hold = $apputils->simplereq(
715 'open-ils.cstore.direct.action.hold_request.search.atomic',
716 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
717 return $hold->[0] if ref($hold);
721 sub fetch_related_holds {
724 return $apputils->simplereq(
726 'open-ils.cstore.direct.action.hold_request.search.atomic',
727 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
731 __PACKAGE__->register_method (
732 method => "hold_pull_list",
733 api_name => "open-ils.circ.hold_pull_list.retrieve",
735 Returns a list of holds that need to be "pulled"
740 __PACKAGE__->register_method (
741 method => "hold_pull_list",
742 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
744 Returns a list of hold ID's that need to be "pulled"
751 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
752 my( $reqr, $evt ) = $U->checkses($authtoken);
755 my $org = $reqr->ws_ou || $reqr->home_ou;
756 # the perm locaiton shouldn't really matter here since holds
757 # will exist all over and VIEW_HOLDS should be universal
758 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
761 if( $self->api_name =~ /id_list/ ) {
762 return $U->storagereq(
763 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
764 $org, $limit, $offset );
766 return $U->storagereq(
767 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
768 $org, $limit, $offset );
772 __PACKAGE__->register_method (
773 method => 'fetch_hold_notify',
774 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
776 Returns a list of hold notification objects based on hold id.
777 @param authtoken The loggin session key
778 @param holdid The id of the hold whose notifications we want to retrieve
779 @return An array of hold notification objects, event on error.
783 sub fetch_hold_notify {
784 my( $self, $conn, $authtoken, $holdid ) = @_;
785 my( $requestor, $evt ) = $U->checkses($authtoken);
788 ($hold, $evt) = $U->fetch_hold($holdid);
790 ($patron, $evt) = $U->fetch_user($hold->usr);
793 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
796 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
797 return $U->cstorereq(
798 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
802 __PACKAGE__->register_method (
803 method => 'create_hold_notify',
804 api_name => 'open-ils.circ.hold_notification.create',
806 Creates a new hold notification object
807 @param authtoken The login session key
808 @param notification The hold notification object to create
809 @return ID of the new object on success, Event on error
812 sub create_hold_notify {
813 my( $self, $conn, $authtoken, $notification ) = @_;
814 my( $requestor, $evt ) = $U->checkses($authtoken);
817 ($hold, $evt) = $U->fetch_hold($notification->hold);
819 ($patron, $evt) = $U->fetch_user($hold->usr);
822 # XXX perm depth probably doesn't matter here -- should always be consortium level
823 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
826 # Set the proper notifier
827 $notification->notify_staff($requestor->id);
828 my $id = $U->storagereq(
829 'open-ils.storage.direct.action.hold_notification.create', $notification );
830 return $U->DB_UPDATE_FAILED($notification) unless $id;
831 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
836 __PACKAGE__->register_method(
837 method => 'reset_hold',
838 api_name => 'open-ils.circ.hold.reset',
840 Un-captures and un-targets a hold, essentially returning
841 it to the state it was in directly after it was placed,
842 then attempts to re-target the hold
843 @param authtoken The login session key
844 @param holdid The id of the hold
850 my( $self, $conn, $auth, $holdid ) = @_;
852 my ($hold, $evt) = $U->fetch_hold($holdid);
854 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
856 $evt = $self->_reset_hold($reqr, $hold);
862 my ($self, $reqr, $hold) = @_;
864 my $e = new_editor(xact =>1, requestor => $reqr);
866 $logger->info("reseting hold ".$hold->id);
870 if( $hold->capture_time and $hold->current_copy ) {
872 my $copy = $e->retrieve_asset_copy($hold->current_copy)
875 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
876 $logger->info("setting copy to status 'reshelving' on hold retarget");
877 $copy->status(OILS_COPY_STATUS_RESHELVING);
878 $copy->editor($e->requestor->id);
879 $copy->edit_date('now');
880 $e->update_asset_copy($copy) or return $e->event;
882 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
884 # We don't want the copy to remain "in transit"
885 $copy->status(OILS_COPY_STATUS_RESHELVING);
886 $logger->warn("! reseting hold [$hid] that is in transit");
887 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
890 my $trans = $e->retrieve_action_transit_copy($transid);
892 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
893 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
894 $logger->info("Transit abort completed with result $evt");
895 return $evt unless "$evt" eq 1;
901 $hold->clear_capture_time;
902 $hold->clear_current_copy;
904 $e->update_action_hold_request($hold) or return $e->event;
908 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
914 __PACKAGE__->register_method(
915 method => 'fetch_open_title_holds',
916 api_name => 'open-ils.circ.open_holds.retrieve',
918 Returns a list ids of un-fulfilled holds for a given title id
919 @param authtoken The login session key
920 @param id the id of the item whose holds we want to retrieve
921 @param type The hold type - M, T, V, C
925 sub fetch_open_title_holds {
926 my( $self, $conn, $auth, $id, $type, $org ) = @_;
927 my $e = new_editor( authtoken => $auth );
928 return $e->event unless $e->checkauth;
931 $org ||= $e->requestor->ws_ou;
933 # return $e->search_action_hold_request(
934 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
936 # XXX make me return IDs in the future ^--
937 my $holds = $e->search_action_hold_request(
940 cancel_time => undef,
942 fulfillment_time => undef
946 flesh_hold_transits($holds);
951 sub flesh_hold_transits {
953 for my $hold ( @$holds ) {
955 $apputils->simplereq(
957 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
958 { hold => $hold->id },
959 { order_by => { ahtc => 'id desc' }, limit => 1 }
965 sub flesh_hold_notices {
966 my( $holds, $e ) = @_;
969 for my $hold (@$holds) {
970 my $notices = $e->search_action_hold_notification(
972 { hold => $hold->id },
973 { order_by => { anh => { 'notify_time desc' } } },
978 $hold->notify_count(scalar(@$notices));
980 my $n = $e->retrieve_action_hold_notification($$notices[0])
982 $hold->notify_time($n->notify_time);
990 __PACKAGE__->register_method(
991 method => 'fetch_captured_holds',
992 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
994 Returns a list of un-fulfilled holds for a given title id
995 @param authtoken The login session key
996 @param org The org id of the location in question
1000 __PACKAGE__->register_method(
1001 method => 'fetch_captured_holds',
1002 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1004 Returns a list ids of un-fulfilled holds for a given title id
1005 @param authtoken The login session key
1006 @param org The org id of the location in question
1010 sub fetch_captured_holds {
1011 my( $self, $conn, $auth, $org ) = @_;
1013 my $e = new_editor(authtoken => $auth);
1014 return $e->event unless $e->checkauth;
1015 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1017 $org ||= $e->requestor->ws_ou;
1019 my $holds = $e->search_action_hold_request(
1021 capture_time => { "!=" => undef },
1022 current_copy => { "!=" => undef },
1023 fulfillment_time => undef,
1025 cancel_time => undef,
1030 for my $h (@$holds) {
1031 my $copy = $e->retrieve_asset_copy($h->current_copy)
1032 or return $e->event;
1034 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1037 if( ! $self->api_name =~ /id_list/ ) {
1038 flesh_hold_transits(\@res);
1039 flesh_hold_notices(\@res, $e);
1042 if( $self->api_name =~ /id_list/ ) {
1043 return [ map { $_->id } @res ];
1050 __PACKAGE__->register_method(
1051 method => "check_title_hold",
1052 api_name => "open-ils.circ.title_hold.is_possible",
1054 Determines if a hold were to be placed by a given user,
1055 whether or not said hold would have any potential copies
1057 @param authtoken The login session key
1058 @param params A hash of named params including:
1059 patronid - the id of the hold recipient
1060 titleid (brn) - the id of the title to be held
1061 depth - the hold range depth (defaults to 0)
1064 sub check_title_hold {
1065 my( $self, $client, $authtoken, $params ) = @_;
1067 my %params = %$params;
1068 my $titleid = $params{titleid} ||"";
1069 my $volid = $params{volume_id};
1070 my $copyid = $params{copy_id};
1071 my $mrid = $params{mrid} ||"";
1072 my $depth = $params{depth} || 0;
1073 my $pickup_lib = $params{pickup_lib};
1074 my $hold_type = $params{hold_type} || 'T';
1076 my $e = new_editor(authtoken=>$authtoken);
1077 return $e->event unless $e->checkauth;
1078 my $patron = $e->retrieve_actor_user($params{patronid})
1079 or return $e->event;
1081 if( $e->requestor->id ne $patron->id ) {
1082 return $e->event unless
1083 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1086 return OpenILS::Event->new('PATRON_BARRED')
1087 if $patron->barred and
1088 ($patron->barred =~ /t/i or $patron->barred == 1);
1090 my $rangelib = $params{range_lib} || $patron->home_ou;
1092 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1093 or return $e->event;
1095 $logger->info("checking hold possibility with type $hold_type");
1101 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1103 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1104 $volume = $e->retrieve_asset_call_number($copy->call_number)
1105 or return $e->event;
1106 $title = $e->retrieve_biblio_record_entry($volume->record)
1107 or return $e->event;
1108 return verify_copy_for_hold(
1109 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1111 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1113 $volume = $e->retrieve_asset_call_number($volid)
1114 or return $e->event;
1115 $title = $e->retrieve_biblio_record_entry($volume->record)
1116 or return $e->event;
1118 return _check_volume_hold_is_possible(
1119 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1121 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1123 return _check_title_hold_is_possible(
1124 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1126 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1128 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1129 my @recs = map { $_->source } @$maps;
1130 for my $rec (@recs) {
1131 return 1 if (_check_title_hold_is_possible(
1132 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1140 sub _check_title_hold_is_possible {
1141 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1147 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1149 while( $title = $U->storagereq(
1150 'open-ils.storage.biblio.record_entry.ranged_tree',
1151 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1155 ref($title->call_numbers) and
1156 @{$title->call_numbers};
1158 for my $cn (@{$title->call_numbers}) {
1160 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1162 for my $copy (@{$cn->copies}) {
1163 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1164 return 1 if verify_copy_for_hold(
1165 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1166 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1175 sub _check_volume_hold_is_possible {
1176 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1177 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1178 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1179 for my $copy ( @$copies ) {
1180 return 1 if verify_copy_for_hold(
1181 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1188 sub verify_copy_for_hold {
1189 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1190 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1191 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1192 { patron => $patron,
1193 requestor => $requestor,
1196 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1197 pickup_lib => $pickup_lib,
1198 request_lib => $request_lib
1206 sub find_nearest_permitted_hold {
1209 my $session = shift;
1212 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1214 # first see if this copy has already been selected to fulfill a hold
1215 my $hold = $session->request(
1216 "open-ils.storage.direct.action.hold_request.search_where",
1217 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1220 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1224 # We know this hold is permitted, so just return it
1225 return $hold if $hold;
1227 $logger->debug("searching for potential holds at org ".
1228 $user->ws_ou." and copy ".$copy->id);
1230 my $holds = $session->request(
1231 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1232 $user->ws_ou, $copy->id, 5 )->gather(1);
1234 return (undef, $evt) unless @$holds;
1236 # for each potential hold, we have to run the permit script
1237 # to make sure the hold is actually permitted.
1239 for my $holdid (@$holds) {
1240 next unless $holdid;
1241 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1243 my ($hold) = $U->fetch_hold($holdid);
1245 my ($reqr) = $U->fetch_user($hold->requestor);
1247 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1249 patron_id => $hold->usr,
1250 requestor => $reqr->id,
1252 pickup_lib => $hold->pickup_lib,
1253 request_lib => $hold->request_lib
1258 return (undef, $evt);
1266 __PACKAGE__->register_method(
1267 method => 'all_rec_holds',
1268 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1272 my( $self, $conn, $auth, $title_id, $args ) = @_;
1274 my $e = new_editor(authtoken=>$auth);
1275 $e->checkauth or return $e->event;
1276 $e->allowed('VIEW_HOLD') or return $e->event;
1278 $args ||= { fulfillment_time => undef };
1279 $args->{cancel_time} = undef;
1281 my $resp = { volume_holds => [], copy_holds => [] };
1283 $resp->{title_holds} = $e->search_action_hold_request(
1285 hold_type => OILS_HOLD_TYPE_TITLE,
1286 target => $title_id,
1290 my $vols = $e->search_asset_call_number(
1291 { record => $title_id, deleted => 'f' }, {idlist=>1});
1293 return $resp unless @$vols;
1295 $resp->{volume_holds} = $e->search_action_hold_request(
1297 hold_type => OILS_HOLD_TYPE_VOLUME,
1302 my $copies = $e->search_asset_copy(
1303 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1305 return $resp unless @$copies;
1307 $resp->{copy_holds} = $e->search_action_hold_request(
1309 hold_type => OILS_HOLD_TYPE_COPY,
1321 __PACKAGE__->register_method(
1322 method => 'uber_hold',
1323 api_name => 'open-ils.circ.hold.details.retrieve'
1327 my($self, $client, $auth, $hold_id) = @_;
1328 my $e = new_editor(authtoken=>$auth);
1329 $e->checkauth or return $e->event;
1330 $e->allowed('VIEW_HOLD') or return $e->event;
1334 my $hold = $e->retrieve_action_hold_request(
1339 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1342 ) or return $e->event;
1344 my $user = $hold->usr;
1345 $hold->usr($user->id);
1347 my $card = $e->retrieve_actor_card($user->card)
1348 or return $e->event;
1350 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1352 flesh_hold_notices([$hold], $e);
1353 flesh_hold_transits([$hold]);
1360 status => _hold_status($e, $hold),
1361 patron_first => $user->first_given_name,
1362 patron_last => $user->family_name,
1363 patron_barcode => $card->barcode,
1369 # -----------------------------------------------------
1370 # Returns the MVR object that represents what the
1372 # -----------------------------------------------------
1374 my( $e, $hold ) = @_;
1380 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1381 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1382 or return $e->event;
1383 $tid = $mr->master_record;
1385 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1386 $tid = $hold->target;
1388 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1389 $volume = $e->retrieve_asset_call_number($hold->target)
1390 or return $e->event;
1391 $tid = $volume->record;
1393 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1394 $copy = $e->retrieve_asset_copy($hold->target)
1395 or return $e->event;
1396 $volume = $e->retrieve_asset_call_number($copy->call_number)
1397 or return $e->event;
1398 $tid = $volume->record;
1401 if(!$copy and ref $hold->current_copy ) {
1402 $copy = $hold->current_copy;
1403 $hold->current_copy($copy->id);
1406 if(!$volume and $copy) {
1407 $volume = $e->retrieve_asset_call_number($copy->call_number);
1410 my $title = $e->retrieve_biblio_record_entry($tid);
1411 return ( $U->record_to_mvr($title), $volume, $copy );