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->selection_ou($recipient->home_ou) unless $hold->selection_ou;
143 $hold = $e->create_action_hold_request($hold) or return $e->event;
144 push( @copyholds, $hold ) if $hold->hold_type eq OILS_HOLD_TYPE_COPY;
149 # Go ahead and target the copy-level holds
151 'open-ils.storage.action.hold_request.copy_targeter',
152 undef, $_->id ) for @copyholds;
158 my( $self, $client, $login_session, @holds) = @_;
160 if(!@holds){return 0;}
161 my( $user, $evt ) = $apputils->checkses($login_session);
165 if(ref($holds[0]) eq 'ARRAY') {
167 } else { $holds = [ @holds ]; }
169 $logger->debug("Iterating over holds requests...");
171 for my $hold (@$holds) {
174 my $type = $hold->hold_type;
176 $logger->activity("User " . $user->id .
177 " creating new hold of type $type for user " . $hold->usr);
180 if($user->id ne $hold->usr) {
181 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
191 # am I allowed to place holds for this user?
192 if($hold->requestor ne $hold->usr) {
193 $perm = _check_request_holds_perm($user->id, $user->home_ou);
194 if($perm) { return $perm; }
197 # is this user allowed to have holds of this type?
198 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
200 #if there is a requestor, see if the requestor has override privelages
201 if($hold->requestor ne $hold->usr) {
202 $perm = _check_request_holds_override($user->id, $user->home_ou);
203 if($perm) {return $perm;}
210 #enforce the fact that the login is the one requesting the hold
211 $hold->requestor($user->id);
212 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
214 my $resp = $apputils->simplereq(
216 'open-ils.storage.direct.action.hold_request.create', $hold );
219 return OpenSRF::EX::ERROR ("Error creating hold");
226 # makes sure that a user has permission to place the type of requested hold
227 # returns the Perm exception if not allowed, returns undef if all is well
228 sub _check_holds_perm {
229 my($type, $user_id, $org_id) = @_;
233 if($evt = $apputils->check_perms(
234 $user_id, $org_id, "MR_HOLDS")) {
238 } elsif ($type eq "T") {
239 if($evt = $apputils->check_perms(
240 $user_id, $org_id, "TITLE_HOLDS")) {
244 } elsif($type eq "V") {
245 if($evt = $apputils->check_perms(
246 $user_id, $org_id, "VOLUME_HOLDS")) {
250 } elsif($type eq "C") {
251 if($evt = $apputils->check_perms(
252 $user_id, $org_id, "COPY_HOLDS")) {
260 # tests if the given user is allowed to place holds on another's behalf
261 sub _check_request_holds_perm {
264 if(my $evt = $apputils->check_perms(
265 $user_id, $org_id, "REQUEST_HOLDS")) {
270 sub _check_request_holds_override {
273 if(my $evt = $apputils->check_perms(
274 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
279 __PACKAGE__->register_method(
280 method => "retrieve_holds_by_id",
281 api_name => "open-ils.circ.holds.retrieve_by_id",
283 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
284 different from the user, then the requestor must have VIEW_HOLD permissions.
288 sub retrieve_holds_by_id {
289 my($self, $client, $auth, $hold_id) = @_;
290 my $e = new_editor(authtoken=>$auth);
291 $e->checkauth or return $e->event;
292 $e->allowed('VIEW_HOLD') or return $e->event;
294 my $holds = $e->search_action_hold_request(
296 { id => $hold_id , fulfillment_time => undef },
297 { order_by => { ahr => "request_time" } }
301 flesh_hold_transits($holds);
302 flesh_hold_notices($holds, $e);
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.
316 __PACKAGE__->register_method(
317 method => "retrieve_holds",
318 api_name => "open-ils.circ.holds.id_list.retrieve",
320 Retrieves all the hold ids for the specified
321 user id. The login session is the requestor and if the requestor is
322 different from the user, then the requestor must have VIEW_HOLD permissions.
326 my($self, $client, $login_session, $user_id) = @_;
328 my( $user, $target, $evt ) = $apputils->checkses_requestor(
329 $login_session, $user_id, 'VIEW_HOLD' );
332 my $holds = $apputils->simplereq(
334 "open-ils.cstore.direct.action.hold_request.search.atomic",
337 fulfillment_time => undef,
338 cancel_time => undef,
340 { order_by => { ahr => "request_time" } }
343 if( ! $self->api_name =~ /id_list/ ) {
344 for my $hold ( @$holds ) {
346 $apputils->simplereq(
348 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
349 { hold => $hold->id },
350 { order_by => { ahtc => 'id desc' }, limit => 1 }
356 if( $self->api_name =~ /id_list/ ) {
357 return [ map { $_->id } @$holds ];
363 __PACKAGE__->register_method(
364 method => "retrieve_holds_by_pickup_lib",
365 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
367 Retrieves all the holds, with hold transits attached, for the specified
371 __PACKAGE__->register_method(
372 method => "retrieve_holds_by_pickup_lib",
373 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
375 Retrieves all the hold ids for the specified
379 sub retrieve_holds_by_pickup_lib {
380 my($self, $client, $login_session, $ou_id) = @_;
382 #FIXME -- put an appropriate permission check here
383 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
384 # $login_session, $user_id, 'VIEW_HOLD' );
385 #return $evt if $evt;
387 my $holds = $apputils->simplereq(
389 "open-ils.cstore.direct.action.hold_request.search.atomic",
391 pickup_lib => $ou_id ,
392 fulfillment_time => undef,
395 { order_by => { ahr => "request_time" } });
398 if( ! $self->api_name =~ /id_list/ ) {
399 flesh_hold_transits($holds);
402 if( $self->api_name =~ /id_list/ ) {
403 return [ map { $_->id } @$holds ];
409 __PACKAGE__->register_method(
410 method => "cancel_hold",
411 api_name => "open-ils.circ.hold.cancel",
413 Cancels the specified hold. The login session
414 is the requestor and if the requestor is different from the usr field
415 on the hold, the requestor must have CANCEL_HOLDS permissions.
416 the hold may be either the hold object or the hold id
420 my($self, $client, $auth, $holdid) = @_;
422 my $e = new_editor(authtoken=>$auth, xact=>1);
423 return $e->event unless $e->checkauth;
425 my $hold = $e->retrieve_action_hold_request($holdid)
428 if( $e->requestor->id ne $hold->usr ) {
429 return $e->event unless $e->allowed('CANCEL_HOLDS');
432 return 1 if $hold->cancel_time;
434 # If the hold is captured, reset the copy status
435 if( $hold->capture_time and $hold->current_copy ) {
437 my $copy = $e->retrieve_asset_copy($hold->current_copy)
440 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
441 $logger->info("setting copy to status 'reshelving' on hold cancel");
442 $copy->status(OILS_COPY_STATUS_RESHELVING);
443 $copy->editor($e->requestor->id);
444 $copy->edit_date('now');
445 $e->update_asset_copy($copy) or return $e->event;
447 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
450 # We don't want the copy to remain "in transit"
451 $copy->status(OILS_COPY_STATUS_RESHELVING);
452 $logger->warn("! canceling hold [$hid] that is in transit");
453 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
456 my $trans = $e->retrieve_action_transit_copy($transid);
458 $logger->info("Aborting transit [$transid] on hold [$hid] cancel...");
459 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
460 $logger->info("Transit abort completed with result $evt");
461 return $evt unless "$evt" eq 1;
467 $hold->cancel_time('now');
468 $e->update_action_hold_request($hold)
471 $self->delete_hold_copy_maps($e, $hold->id);
477 sub delete_hold_copy_maps {
482 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
484 $editor->delete_action_hold_copy_map($_)
485 or return $editor->event;
491 __PACKAGE__->register_method(
492 method => "update_hold",
493 api_name => "open-ils.circ.hold.update",
495 Updates the specified hold. The login session
496 is the requestor and if the requestor is different from the usr field
497 on the hold, the requestor must have UPDATE_HOLDS permissions.
501 my($self, $client, $login_session, $hold) = @_;
503 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
504 $login_session, $hold->usr, 'UPDATE_HOLD' );
507 $logger->activity('User ' . $requestor->id .
508 ' updating hold ' . $hold->id . ' for user ' . $target->id );
510 return $U->storagereq(
511 "open-ils.storage.direct.action.hold_request.update", $hold );
515 __PACKAGE__->register_method(
516 method => "retrieve_hold_status",
517 api_name => "open-ils.circ.hold.status.retrieve",
519 Calculates the current status of the hold.
520 the requestor must have VIEW_HOLD permissions if the hold is for a user
521 other than the requestor.
522 Returns -1 on error (for now)
523 Returns 1 for 'waiting for copy to become available'
524 Returns 2 for 'waiting for copy capture'
525 Returns 3 for 'in transit'
526 Returns 4 for 'arrived'
529 sub retrieve_hold_status {
530 my($self, $client, $auth, $hold_id) = @_;
532 my $e = new_editor(authtoken => $auth);
533 return $e->event unless $e->checkauth;
534 my $hold = $e->retrieve_action_hold_request($hold_id)
537 if( $e->requestor->id != $hold->usr ) {
538 return $e->event unless $e->allowed('VIEW_HOLD');
541 return _hold_status($e, $hold);
547 return 1 unless $hold->current_copy;
548 return 2 unless $hold->capture_time;
550 my $copy = $e->retrieve_asset_copy($hold->current_copy)
553 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
554 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
564 __PACKAGE__->register_method(
565 method => "capture_copy",
566 api_name => "open-ils.circ.hold.capture_copy.barcode",
568 Captures a copy to fulfil a hold
569 Params is login session and copy barcode
570 Optional param is 'flesh'. If set, we also return the
571 relevant copy and title
572 login mus have COPY_CHECKIN permissions (since this is essentially
576 # XXX deprecate me XXX
579 my( $self, $client, $login_session, $params ) = @_;
580 my %params = %$params;
581 my $barcode = $params{barcode};
584 my( $user, $target, $copy, $hold, $evt );
586 ( $user, $evt ) = $apputils->checkses($login_session);
589 # am I allowed to checkin a copy?
590 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
593 $logger->info("Capturing copy with barcode $barcode");
595 my $session = $apputils->start_db_session();
597 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
600 $logger->debug("Capturing copy " . $copy->id);
602 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
603 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
606 warn "Found hold " . $hold->id . "\n";
607 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
609 $hold->current_copy($copy->id);
610 $hold->capture_time("now");
613 my $stat = $session->request(
614 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
615 if(!$stat) { throw OpenSRF::EX::ERROR
616 ("Error updating hold request " . $copy->id); }
618 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
620 # if the staff member capturing this item is not at the pickup lib
621 if( $user->home_ou ne $hold->pickup_lib ) {
622 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
625 $copy->editor($user->id);
626 $copy->edit_date("now");
627 $stat = $session->request(
628 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
629 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
631 my $payload = { hold => $hold };
632 $payload->{copy} = $copy if $params{flesh_copy};
634 if($params{flesh_record}) {
636 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
638 $record = $apputils->record_to_mvr($record);
639 $payload->{record} = $record;
642 $apputils->commit_db_session($session);
644 return OpenILS::Event->new('ROUTE_ITEM',
645 route_to => $hold->pickup_lib, payload => $payload );
648 sub _build_hold_transit {
649 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
650 my $trans = Fieldmapper::action::hold_transit_copy->new;
652 $trans->hold($hold->id);
653 $trans->source($user->home_ou);
654 $trans->dest($hold->pickup_lib);
655 $trans->source_send_time("now");
656 $trans->target_copy($copy->id);
657 $trans->copy_status($copy->status);
659 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
660 my ($stat) = $meth->run( $login_session, $trans, $session );
661 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
662 else { $copy->status(6); } #status in transit
667 __PACKAGE__->register_method(
668 method => "create_hold_transit",
669 api_name => "open-ils.circ.hold_transit.create",
671 Creates a new transit object
674 sub create_hold_transit {
675 my( $self, $client, $login_session, $transit, $session ) = @_;
677 my( $user, $evt ) = $apputils->checkses($login_session);
679 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
683 if($session) { $ses = $session; }
684 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
686 return $ses->request(
687 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
693 sub find_local_hold {
694 my( $class, $session, $copy, $user ) = @_;
695 return $class->find_nearest_permitted_hold($session, $copy, $user);
703 sub fetch_open_hold_by_current_copy {
706 my $hold = $apputils->simplereq(
708 'open-ils.cstore.direct.action.hold_request.search.atomic',
709 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
710 return $hold->[0] if ref($hold);
714 sub fetch_related_holds {
717 return $apputils->simplereq(
719 'open-ils.cstore.direct.action.hold_request.search.atomic',
720 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
724 __PACKAGE__->register_method (
725 method => "hold_pull_list",
726 api_name => "open-ils.circ.hold_pull_list.retrieve",
728 Returns a list of holds that need to be "pulled"
733 __PACKAGE__->register_method (
734 method => "hold_pull_list",
735 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
737 Returns a list of hold ID's that need to be "pulled"
744 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
745 my( $reqr, $evt ) = $U->checkses($authtoken);
748 my $org = $reqr->ws_ou || $reqr->home_ou;
749 # the perm locaiton shouldn't really matter here since holds
750 # will exist all over and VIEW_HOLDS should be universal
751 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
754 if( $self->api_name =~ /id_list/ ) {
755 return $U->storagereq(
756 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
757 $org, $limit, $offset );
759 return $U->storagereq(
760 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
761 $org, $limit, $offset );
765 __PACKAGE__->register_method (
766 method => 'fetch_hold_notify',
767 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
769 Returns a list of hold notification objects based on hold id.
770 @param authtoken The loggin session key
771 @param holdid The id of the hold whose notifications we want to retrieve
772 @return An array of hold notification objects, event on error.
776 sub fetch_hold_notify {
777 my( $self, $conn, $authtoken, $holdid ) = @_;
778 my( $requestor, $evt ) = $U->checkses($authtoken);
781 ($hold, $evt) = $U->fetch_hold($holdid);
783 ($patron, $evt) = $U->fetch_user($hold->usr);
786 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
789 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
790 return $U->cstorereq(
791 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
795 __PACKAGE__->register_method (
796 method => 'create_hold_notify',
797 api_name => 'open-ils.circ.hold_notification.create',
799 Creates a new hold notification object
800 @param authtoken The login session key
801 @param notification The hold notification object to create
802 @return ID of the new object on success, Event on error
805 sub create_hold_notify {
806 my( $self, $conn, $authtoken, $notification ) = @_;
807 my( $requestor, $evt ) = $U->checkses($authtoken);
810 ($hold, $evt) = $U->fetch_hold($notification->hold);
812 ($patron, $evt) = $U->fetch_user($hold->usr);
815 # XXX perm depth probably doesn't matter here -- should always be consortium level
816 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
819 # Set the proper notifier
820 $notification->notify_staff($requestor->id);
821 my $id = $U->storagereq(
822 'open-ils.storage.direct.action.hold_notification.create', $notification );
823 return $U->DB_UPDATE_FAILED($notification) unless $id;
824 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
829 __PACKAGE__->register_method(
830 method => 'reset_hold',
831 api_name => 'open-ils.circ.hold.reset',
833 Un-captures and un-targets a hold, essentially returning
834 it to the state it was in directly after it was placed,
835 then attempts to re-target the hold
836 @param authtoken The login session key
837 @param holdid The id of the hold
843 my( $self, $conn, $auth, $holdid ) = @_;
845 my ($hold, $evt) = $U->fetch_hold($holdid);
847 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
849 $evt = $self->_reset_hold($reqr, $hold);
855 my ($self, $reqr, $hold) = @_;
857 my $e = new_editor(xact =>1, requestor => $reqr);
859 $logger->info("reseting hold ".$hold->id);
863 if( $hold->capture_time and $hold->current_copy ) {
865 my $copy = $e->retrieve_asset_copy($hold->current_copy)
868 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
869 $logger->info("setting copy to status 'reshelving' on hold retarget");
870 $copy->status(OILS_COPY_STATUS_RESHELVING);
871 $copy->editor($e->requestor->id);
872 $copy->edit_date('now');
873 $e->update_asset_copy($copy) or return $e->event;
875 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
877 # We don't want the copy to remain "in transit"
878 $copy->status(OILS_COPY_STATUS_RESHELVING);
879 $logger->warn("! reseting hold [$hid] that is in transit");
880 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
883 my $trans = $e->retrieve_action_transit_copy($transid);
885 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
886 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
887 $logger->info("Transit abort completed with result $evt");
888 return $evt unless "$evt" eq 1;
894 $hold->clear_capture_time;
895 $hold->clear_current_copy;
897 $e->update_action_hold_request($hold) or return $e->event;
901 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
907 __PACKAGE__->register_method(
908 method => 'fetch_open_title_holds',
909 api_name => 'open-ils.circ.open_holds.retrieve',
911 Returns a list ids of un-fulfilled holds for a given title id
912 @param authtoken The login session key
913 @param id the id of the item whose holds we want to retrieve
914 @param type The hold type - M, T, V, C
918 sub fetch_open_title_holds {
919 my( $self, $conn, $auth, $id, $type, $org ) = @_;
920 my $e = new_editor( authtoken => $auth );
921 return $e->event unless $e->checkauth;
924 $org ||= $e->requestor->ws_ou;
926 # return $e->search_action_hold_request(
927 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
929 # XXX make me return IDs in the future ^--
930 my $holds = $e->search_action_hold_request(
933 cancel_time => undef,
935 fulfillment_time => undef
939 flesh_hold_transits($holds);
944 sub flesh_hold_transits {
946 for my $hold ( @$holds ) {
948 $apputils->simplereq(
950 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
951 { hold => $hold->id },
952 { order_by => { ahtc => 'id desc' }, limit => 1 }
958 sub flesh_hold_notices {
959 my( $holds, $e ) = @_;
962 for my $hold (@$holds) {
963 my $notices = $e->search_action_hold_notification(
965 { hold => $hold->id },
966 { order_by => { anh => { 'notify_time desc' } } },
971 $hold->notify_count(scalar(@$notices));
973 my $n = $e->retrieve_action_hold_notification($$notices[0])
975 $hold->notify_time($n->notify_time);
983 __PACKAGE__->register_method(
984 method => 'fetch_captured_holds',
985 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
987 Returns a list of un-fulfilled holds for a given title id
988 @param authtoken The login session key
989 @param org The org id of the location in question
993 __PACKAGE__->register_method(
994 method => 'fetch_captured_holds',
995 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
997 Returns a list ids of un-fulfilled holds for a given title id
998 @param authtoken The login session key
999 @param org The org id of the location in question
1003 sub fetch_captured_holds {
1004 my( $self, $conn, $auth, $org ) = @_;
1006 my $e = new_editor(authtoken => $auth);
1007 return $e->event unless $e->checkauth;
1008 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1010 $org ||= $e->requestor->ws_ou;
1012 my $holds = $e->search_action_hold_request(
1014 capture_time => { "!=" => undef },
1015 current_copy => { "!=" => undef },
1016 fulfillment_time => undef,
1018 cancel_time => undef,
1023 for my $h (@$holds) {
1024 my $copy = $e->retrieve_asset_copy($h->current_copy)
1025 or return $e->event;
1027 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1030 if( ! $self->api_name =~ /id_list/ ) {
1031 flesh_hold_transits(\@res);
1032 flesh_hold_notices(\@res, $e);
1035 if( $self->api_name =~ /id_list/ ) {
1036 return [ map { $_->id } @res ];
1043 __PACKAGE__->register_method(
1044 method => "check_title_hold",
1045 api_name => "open-ils.circ.title_hold.is_possible",
1047 Determines if a hold were to be placed by a given user,
1048 whether or not said hold would have any potential copies
1050 @param authtoken The login session key
1051 @param params A hash of named params including:
1052 patronid - the id of the hold recipient
1053 titleid (brn) - the id of the title to be held
1054 depth - the hold range depth (defaults to 0)
1057 sub check_title_hold {
1058 my( $self, $client, $authtoken, $params ) = @_;
1060 my %params = %$params;
1061 my $titleid = $params{titleid} ||"";
1062 my $mrid = $params{mrid} ||"";
1063 my $depth = $params{depth} || 0;
1064 my $pickup_lib = $params{pickup_lib};
1065 my $hold_type = $params{hold_type} || 'T';
1067 my $e = new_editor(authtoken=>$authtoken);
1068 return $e->event unless $e->checkauth;
1069 my $patron = $e->retrieve_actor_user($params{patronid})
1070 or return $e->event;
1072 if( $e->requestor->id ne $patron->id ) {
1073 return $e->event unless
1074 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1077 return OpenILS::Event->new('PATRON_BARRED')
1078 if $patron->barred and
1079 ($patron->barred =~ /t/i or $patron->barred == 1);
1081 my $rangelib = $params{range_lib} || $patron->home_ou;
1083 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1084 or return $e->event;
1086 if( $hold_type eq 'T' ) {
1087 return _check_title_hold_is_possible(
1088 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1091 if( $hold_type eq 'M' ) {
1092 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1093 my @recs = map { $_->source } @$maps;
1094 for my $rec (@recs) {
1095 return 1 if (_check_title_hold_is_possible(
1096 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1103 sub _check_title_hold_is_possible {
1104 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1110 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1112 while( $title = $U->storagereq(
1113 'open-ils.storage.biblio.record_entry.ranged_tree',
1114 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1118 ref($title->call_numbers) and
1119 @{$title->call_numbers};
1121 for my $cn (@{$title->call_numbers}) {
1123 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1125 for my $copy (@{$cn->copies}) {
1127 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1129 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1130 { patron => $patron,
1131 requestor => $requestor,
1134 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1135 pickup_lib => $pickup_lib,
1136 request_lib => $request_lib
1140 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1151 sub find_nearest_permitted_hold {
1154 my $session = shift;
1157 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1159 # first see if this copy has already been selected to fulfill a hold
1160 my $hold = $session->request(
1161 "open-ils.storage.direct.action.hold_request.search_where",
1162 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1165 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1169 # We know this hold is permitted, so just return it
1170 return $hold if $hold;
1172 $logger->debug("searching for potential holds at org ".
1173 $user->ws_ou." and copy ".$copy->id);
1175 my $holds = $session->request(
1176 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1177 $user->ws_ou, $copy->id, 5 )->gather(1);
1179 return (undef, $evt) unless @$holds;
1181 # for each potential hold, we have to run the permit script
1182 # to make sure the hold is actually permitted.
1184 for my $holdid (@$holds) {
1185 next unless $holdid;
1186 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1188 my ($hold) = $U->fetch_hold($holdid);
1190 my ($reqr) = $U->fetch_user($hold->requestor);
1192 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1194 patron_id => $hold->usr,
1195 requestor => $reqr->id,
1197 pickup_lib => $hold->pickup_lib,
1198 request_lib => $hold->request_lib
1203 return (undef, $evt);
1207 __PACKAGE__->register_method(
1208 method => 'all_rec_holds',
1209 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1213 my( $self, $conn, $auth, $title_id, $args ) = @_;
1215 my $e = new_editor(authtoken=>$auth);
1216 $e->checkauth or return $e->event;
1217 $e->allowed('VIEW_HOLD') or return $e->event;
1219 $args ||= { fulfillment_time => undef };
1220 $args->{cancel_time} = undef;
1222 my $resp = { volume_holds => [], copy_holds => [] };
1224 $resp->{title_holds} = $e->search_action_hold_request(
1226 hold_type => OILS_HOLD_TYPE_TITLE,
1227 target => $title_id,
1231 my $vols = $e->search_asset_call_number(
1232 { record => $title_id, deleted => 'f' }, {idlist=>1});
1234 return $resp unless @$vols;
1236 $resp->{volume_holds} = $e->search_action_hold_request(
1238 hold_type => OILS_HOLD_TYPE_VOLUME,
1243 my $copies = $e->search_asset_copy(
1244 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1246 return $resp unless @$copies;
1248 $resp->{copy_holds} = $e->search_action_hold_request(
1250 hold_type => OILS_HOLD_TYPE_COPY,
1260 __PACKAGE__->register_method(
1261 method => 'uber_hold',
1262 api_name => 'open-ils.circ.hold.details.retrieve'
1266 my($self, $client, $auth, $hold_id) = @_;
1267 my $e = new_editor(authtoken=>$auth);
1268 $e->checkauth or return $e->event;
1269 $e->allowed('VIEW_HOLD') or return $e->event;
1273 my $hold = $e->retrieve_action_hold_request(
1278 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1281 ) or return $e->event;
1283 my $user = $hold->usr;
1284 $hold->usr($user->id);
1286 my $card = $e->retrieve_actor_card($user->card)
1287 or return $e->event;
1289 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1291 flesh_hold_notices([$hold], $e);
1292 flesh_hold_transits([$hold]);
1299 status => _hold_status($e, $hold),
1300 patron_first => $user->first_given_name,
1301 patron_last => $user->family_name,
1302 patron_barcode => $card->barcode,
1308 # -----------------------------------------------------
1309 # Returns the MVR object that represents what the
1311 # -----------------------------------------------------
1313 my( $e, $hold ) = @_;
1319 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1320 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1321 or return $e->event;
1322 $tid = $mr->master_record;
1324 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1325 $tid = $hold->target;
1327 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1328 $volume = $e->retrieve_asset_call_number($hold->target)
1329 or return $e->event;
1330 $tid = $volume->record;
1332 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1333 $copy = $e->retrieve_asset_copy($hold->target)
1334 or return $e->event;
1335 $volume = $e->retrieve_asset_call_number($copy->call_number)
1336 or return $e->event;
1337 $tid = $volume->record;
1340 if(!$copy and ref $hold->current_copy ) {
1341 $copy = $hold->current_copy;
1342 $hold->current_copy($copy->id);
1345 if(!$volume and $copy) {
1346 $volume = $e->retrieve_asset_call_number($copy->call_number);
1349 my $title = $e->retrieve_biblio_record_entry($tid);
1350 return ( $U->record_to_mvr($title), $volume, $copy );