]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
added support for displaying the last X cancelled holds, most recently cancelled...
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Holds.pm
1 # ---------------------------------------------------------------
2 # Copyright (C) 2005  Georgia Public Library Service 
3 # Bill Erickson <highfalutin@gmail.com>
4
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.
9
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 # ---------------------------------------------------------------
15
16
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
21 use DateTime;
22 use Data::Dumper;
23 use OpenSRF::EX qw(:try);
24 use OpenILS::Perm;
25 use OpenILS::Event;
26 use OpenSRF::Utils;
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::CStoreEditor q/:funcs/;
29 use OpenILS::Utils::PermitHold;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenILS::Const qw/:const/;
32 use OpenILS::Application::Circ::Transit;
33 use OpenILS::Application::Actor::Friends;
34
35 my $apputils = "OpenILS::Application::AppUtils";
36 my $U = $apputils;
37
38
39
40
41 __PACKAGE__->register_method(
42         method  => "create_hold",
43         api_name        => "open-ils.circ.holds.create",
44         notes           => <<NOTE);
45 Create a new hold for an item.  From a permissions perspective, 
46 the login session is used as the 'requestor' of the hold.  
47 The hold recipient is determined by the 'usr' setting within
48 the hold object.
49
50 First we verify the requestion has holds request permissions.
51 Then we verify that the recipient is allowed to make the given hold.
52 If not, we see if the requestor has "override" capabilities.  If not,
53 a permission exception is returned.  If permissions allow, we cycle
54 through the set of holds objects and create.
55
56 If the recipient does not have permission to place multiple holds
57 on a single title and said operation is attempted, a permission
58 exception is returned
59 NOTE
60
61
62 __PACKAGE__->register_method(
63         method  => "create_hold",
64         api_name        => "open-ils.circ.holds.create.override",
65         signature       => q/
66                 If the recipient is not allowed to receive the requested hold,
67                 call this method to attempt the override
68                 @see open-ils.circ.holds.create
69         /
70 );
71
72 sub create_hold {
73         my( $self, $conn, $auth, @holds ) = @_;
74         my $e = new_editor(authtoken=>$auth, xact=>1);
75         return $e->event unless $e->checkauth;
76
77         my $override = 1 if $self->api_name =~ /override/;
78
79         my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
80
81 #       my @copyholds;
82
83         for my $hold (@$holds) {
84
85                 next unless $hold;
86                 my @events;
87
88                 my $requestor = $e->requestor;
89                 my $recipient = $requestor;
90
91
92                 if( $requestor->id ne $hold->usr ) {
93                         # Make sure the requestor is allowed to place holds for 
94                         # the recipient if they are not the same people
95                         $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
96                         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
97                 }
98
99                 # Now make sure the recipient is allowed to receive the specified hold
100                 my $pevt;
101                 my $porg                = $recipient->home_ou;
102                 my $rid         = $e->requestor->id;
103                 my $t                   = $hold->hold_type;
104
105                 # See if a duplicate hold already exists
106                 my $sargs = {
107                         usr                     => $recipient->id, 
108                         hold_type       => $t, 
109                         fulfillment_time => undef, 
110                         target          => $hold->target,
111                         cancel_time     => undef,
112                 };
113
114                 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
115                         
116                 my $existing = $e->search_action_hold_request($sargs); 
117                 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
118
119                 if( $t eq OILS_HOLD_TYPE_METARECORD ) 
120                         { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
121
122                 if( $t eq OILS_HOLD_TYPE_TITLE ) 
123                         { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg);  }
124
125                 if( $t eq OILS_HOLD_TYPE_VOLUME ) 
126                         { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
127
128                 if( $t eq OILS_HOLD_TYPE_COPY ) 
129                         { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
130
131                 return $pevt if $pevt;
132
133                 if( @events ) {
134                         if( $override ) {
135                                 for my $evt (@events) {
136                                         next unless $evt;
137                                         my $name = $evt->{textcode};
138                                         return $e->event unless $e->allowed("$name.override", $porg);
139                                 }
140                         } else {
141                                 return \@events;
142                         }
143                 }
144
145         # set the configured expire time
146         unless($hold->expire_time) {
147             my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
148             if($interval) {
149                 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
150                 $hold->expire_time($U->epoch2ISO8601($date->epoch));
151             }
152         }
153
154                 $hold->requestor($e->requestor->id); 
155                 $hold->request_lib($e->requestor->ws_ou);
156                 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
157                 $hold = $e->create_action_hold_request($hold) or return $e->event;
158         }
159
160         $e->commit;
161
162         $conn->respond_complete(1);
163
164     for(@holds) {
165         next if $U->is_true($_->frozen);
166             $U->storagereq(
167                     'open-ils.storage.action.hold_request.copy_targeter', 
168                     undef, $_->id );
169     }
170
171         return undef;
172 }
173
174 sub __create_hold {
175         my( $self, $client, $login_session, @holds) = @_;
176
177         if(!@holds){return 0;}
178         my( $user, $evt ) = $apputils->checkses($login_session);
179         return $evt if $evt;
180
181         my $holds;
182         if(ref($holds[0]) eq 'ARRAY') {
183                 $holds = $holds[0];
184         } else { $holds = [ @holds ]; }
185
186         $logger->debug("Iterating over holds requests...");
187
188         for my $hold (@$holds) {
189
190                 if(!$hold){next};
191                 my $type = $hold->hold_type;
192
193                 $logger->activity("User " . $user->id . 
194                         " creating new hold of type $type for user " . $hold->usr);
195
196                 my $recipient;
197                 if($user->id ne $hold->usr) {
198                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
199                         return $evt if $evt;
200
201                 } else {
202                         $recipient = $user;
203                 }
204
205
206                 my $perm = undef;
207
208                 # am I allowed to place holds for this user?
209                 if($hold->requestor ne $hold->usr) {
210                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
211                         if($perm) { return $perm; }
212                 }
213
214                 # is this user allowed to have holds of this type?
215                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
216         return $perm if $perm;
217
218                 #enforce the fact that the login is the one requesting the hold
219                 $hold->requestor($user->id); 
220                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
221
222                 my $resp = $apputils->simplereq(
223                         'open-ils.storage',
224                         'open-ils.storage.direct.action.hold_request.create', $hold );
225
226                 if(!$resp) { 
227                         return OpenSRF::EX::ERROR ("Error creating hold"); 
228                 }
229         }
230
231         return 1;
232 }
233
234 # makes sure that a user has permission to place the type of requested hold
235 # returns the Perm exception if not allowed, returns undef if all is well
236 sub _check_holds_perm {
237         my($type, $user_id, $org_id) = @_;
238
239         my $evt;
240         if($type eq "M") {
241                 if($evt = $apputils->check_perms(
242                         $user_id, $org_id, "MR_HOLDS")) {
243                         return $evt;
244                 } 
245
246         } elsif ($type eq "T") {
247                 if($evt = $apputils->check_perms(
248                         $user_id, $org_id, "TITLE_HOLDS")) {
249                         return $evt;
250                 }
251
252         } elsif($type eq "V") {
253                 if($evt = $apputils->check_perms(
254                         $user_id, $org_id, "VOLUME_HOLDS")) {
255                         return $evt;
256                 }
257
258         } elsif($type eq "C") {
259                 if($evt = $apputils->check_perms(
260                         $user_id, $org_id, "COPY_HOLDS")) {
261                         return $evt;
262                 }
263         }
264
265         return undef;
266 }
267
268 # tests if the given user is allowed to place holds on another's behalf
269 sub _check_request_holds_perm {
270         my $user_id = shift;
271         my $org_id = shift;
272         if(my $evt = $apputils->check_perms(
273                 $user_id, $org_id, "REQUEST_HOLDS")) {
274                 return $evt;
275         }
276 }
277
278 __PACKAGE__->register_method(
279         method  => "retrieve_holds_by_id",
280         api_name        => "open-ils.circ.holds.retrieve_by_id",
281         notes           => <<NOTE);
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.
284 NOTE
285
286
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;
292
293         my $holds = $e->search_action_hold_request(
294                 [
295                         { id =>  $hold_id , fulfillment_time => undef }, 
296                         { order_by => { ahr => "request_time" } }
297                 ]
298         );
299
300         flesh_hold_transits($holds);
301         flesh_hold_notices($holds, $e);
302         return $holds;
303 }
304
305
306 __PACKAGE__->register_method(
307         method  => "retrieve_holds",
308         api_name        => "open-ils.circ.holds.retrieve",
309         notes           => <<NOTE);
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.
313 NOTE
314
315 __PACKAGE__->register_method(
316         method  => "retrieve_holds",
317     authoritative => 1,
318         api_name        => "open-ils.circ.holds.id_list.retrieve",
319         notes           => <<NOTE);
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.
323 NOTE
324
325 sub retrieve_holds {
326         my($self, $client, $auth, $user_id, $options) = @_;
327
328     my $e = new_editor(authtoken=>$auth);
329     return $e->event unless $e->checkauth;
330     $user_id = $e->requestor->id unless defined $user_id;
331     $options ||= {};
332
333     unless($user_id == $e->requestor->id) {
334         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
335         unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
336             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
337                 $e, $user_id, $e->requestor->id, 'hold.view');
338             return $e->event unless $allowed;
339         }
340     }
341
342     my $holds = $e->search_action_hold_request([
343                 {   usr =>  $user_id , 
344                         fulfillment_time => undef,
345                         cancel_time => undef,
346                 }, 
347                 {order_by => {ahr => "request_time"}}
348         ]);
349
350     if($$options{canceled}) {
351         my $count = $$options{cancel_count} || 
352             $U->ou_ancestor_setting_value($e->requestor->ws_ou, 
353                 'circ.canceled_hold_display_count', $e) || 5;
354
355         my $canceled = $e->search_action_hold_request([
356                     {   usr =>  $user_id , 
357                             fulfillment_time => undef,
358                             cancel_time => {'!=' => undef},
359                     }, 
360                     {order_by => {ahr => "cancel_time desc"}, limit => $count}
361             ]);
362         push(@$holds, @$canceled);
363     }
364         
365         if( ! $self->api_name =~ /id_list/ ) {
366                 for my $hold ( @$holds ) {
367                         $hold->transit(
368                 $e->search_action_hold_transit_copy([
369                                         {hold => $hold->id},
370                                         {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
371                         );
372                 }
373         }
374
375         if( $self->api_name =~ /id_list/ ) {
376                 return [ map { $_->id } @$holds ];
377         } else {
378                 return $holds;
379         }
380 }
381
382
383 __PACKAGE__->register_method(
384    method => 'user_hold_count',
385    api_name => 'open-ils.circ.hold.user.count');
386
387 sub user_hold_count {
388    my( $self, $conn, $auth, $userid ) = @_;
389    my $e = new_editor(authtoken=>$auth);
390    return $e->event unless $e->checkauth;
391    my $patron = $e->retrieve_actor_user($userid)
392       or return $e->event;
393    return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
394    return __user_hold_count($self, $e, $userid);
395 }
396
397 sub __user_hold_count {
398    my( $self, $e, $userid ) = @_;
399    my $holds = $e->search_action_hold_request(
400       {  usr =>  $userid , 
401          fulfillment_time => undef,
402          cancel_time => undef,
403       }, 
404       {idlist => 1}
405    );
406
407    return scalar(@$holds);
408 }
409
410
411 __PACKAGE__->register_method(
412         method  => "retrieve_holds_by_pickup_lib",
413         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
414         notes           => <<NOTE);
415 Retrieves all the holds, with hold transits attached, for the specified
416 pickup_ou id. 
417 NOTE
418
419 __PACKAGE__->register_method(
420         method  => "retrieve_holds_by_pickup_lib",
421         api_name        => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
422         notes           => <<NOTE);
423 Retrieves all the hold ids for the specified
424 pickup_ou id. 
425 NOTE
426
427 sub retrieve_holds_by_pickup_lib {
428         my($self, $client, $login_session, $ou_id) = @_;
429
430         #FIXME -- put an appropriate permission check here
431         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
432         #       $login_session, $user_id, 'VIEW_HOLD' );
433         #return $evt if $evt;
434
435         my $holds = $apputils->simplereq(
436                 'open-ils.cstore',
437                 "open-ils.cstore.direct.action.hold_request.search.atomic",
438                 { 
439                         pickup_lib =>  $ou_id , 
440                         fulfillment_time => undef,
441                         cancel_time => undef
442                 }, 
443                 { order_by => { ahr => "request_time" } });
444
445
446         if( ! $self->api_name =~ /id_list/ ) {
447                 flesh_hold_transits($holds);
448         }
449
450         if( $self->api_name =~ /id_list/ ) {
451                 return [ map { $_->id } @$holds ];
452         } else {
453                 return $holds;
454         }
455 }
456
457 __PACKAGE__->register_method(
458         method  => "cancel_hold",
459         api_name        => "open-ils.circ.hold.cancel",
460         notes           => <<"  NOTE");
461         Cancels the specified hold.  The login session
462         is the requestor and if the requestor is different from the usr field
463         on the hold, the requestor must have CANCEL_HOLDS permissions.
464         the hold may be either the hold object or the hold id
465         NOTE
466
467 sub cancel_hold {
468         my($self, $client, $auth, $holdid, $cause, $note) = @_;
469
470         my $e = new_editor(authtoken=>$auth, xact=>1);
471         return $e->event unless $e->checkauth;
472
473         my $hold = $e->retrieve_action_hold_request($holdid)
474                 or return $e->event;
475
476         if( $e->requestor->id ne $hold->usr ) {
477                 return $e->event unless $e->allowed('CANCEL_HOLDS');
478         }
479
480         return 1 if $hold->cancel_time;
481
482         # If the hold is captured, reset the copy status
483         if( $hold->capture_time and $hold->current_copy ) {
484
485                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
486                         or return $e->event;
487
488                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
489          $logger->info("canceling hold $holdid whose item is on the holds shelf");
490 #                       $logger->info("setting copy to status 'reshelving' on hold cancel");
491 #                       $copy->status(OILS_COPY_STATUS_RESHELVING);
492 #                       $copy->editor($e->requestor->id);
493 #                       $copy->edit_date('now');
494 #                       $e->update_asset_copy($copy) or return $e->event;
495
496                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
497
498                         my $hid = $hold->id;
499                         $logger->warn("! canceling hold [$hid] that is in transit");
500                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
501
502                         if( $transid ) {
503                                 my $trans = $e->retrieve_action_transit_copy($transid);
504                                 # Leave the transit alive, but  set the copy status to 
505                                 # reshelving so it will be properly reshelved when it gets back home
506                                 if( $trans ) {
507                                         $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
508                                         $e->update_action_transit_copy($trans) or return $e->die_event;
509                                 }
510                         }
511                 }
512         }
513
514         $hold->cancel_time('now');
515     $hold->cancel_cause($cause);
516     $hold->cancel_note($note);
517         $e->update_action_hold_request($hold)
518                 or return $e->event;
519
520         delete_hold_copy_maps($self, $e, $hold->id);
521
522         $e->commit;
523         return 1;
524 }
525
526 sub delete_hold_copy_maps {
527         my $class = shift;
528         my $editor = shift;
529         my $holdid = shift;
530
531         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
532         for(@$maps) {
533                 $editor->delete_action_hold_copy_map($_) 
534                         or return $editor->event;
535         }
536         return undef;
537 }
538
539
540 __PACKAGE__->register_method(
541         method  => "update_hold",
542         api_name        => "open-ils.circ.hold.update",
543         notes           => <<"  NOTE");
544         Updates the specified hold.  The login session
545         is the requestor and if the requestor is different from the usr field
546         on the hold, the requestor must have UPDATE_HOLDS permissions.
547         NOTE
548
549 sub update_hold {
550         my($self, $client, $auth, $hold) = @_;
551
552     my $e = new_editor(authtoken=>$auth, xact=>1);
553     return $e->die_event unless $e->checkauth;
554
555     my $orig_hold = $e->retrieve_action_hold_request($hold->id)
556         or return $e->die_event;
557
558     # don't allow the user to be changed
559     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
560
561     if($hold->usr ne $e->requestor->id) {
562         # if the hold is for a different user, make sure the 
563         # requestor has the appropriate permissions
564         my $usr = $e->retrieve_actor_user($hold->usr)
565             or return $e->die_event;
566         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
567     }
568
569     # --------------------------------------------------------------
570     # if the hold is on the holds shelf and the pickup lib changes, 
571     # we need to create a new transit
572     # --------------------------------------------------------------
573     if( ($orig_hold->pickup_lib ne $hold->pickup_lib) and (_hold_status($e, $hold) == 4)) {
574         return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
575         return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
576         my $evt = transit_hold($e, $orig_hold, $hold, 
577             $e->retrieve_asset_copy($hold->current_copy));
578         return $evt if $evt;
579     }
580
581     update_hold_if_frozen($self, $e, $hold, $orig_hold);
582     $e->update_action_hold_request($hold) or return $e->die_event;
583     $e->commit;
584     return $hold->id;
585 }
586
587 sub transit_hold {
588     my($e, $orig_hold, $hold, $copy) = @_;
589     my $src = $orig_hold->pickup_lib;
590     my $dest = $hold->pickup_lib;
591
592     $logger->info("putting hold into transit on pickup_lib update");
593
594     my $transit = Fieldmapper::action::transit_copy->new;
595     $transit->source($src);
596     $transit->dest($dest);
597     $transit->target_copy($copy->id);
598     $transit->source_send_time('now');
599     $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
600
601     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
602     $copy->editor($e->requestor->id);
603     $copy->edit_date('now');
604
605     $e->create_action_transit_copy($transit) or return $e->die_event;
606     $e->update_asset_copy($copy) or return $e->die_event;
607     return undef;
608 }
609
610 # if the hold is frozen, this method ensures that the hold is not "targeted", 
611 # that is, it clears the current_copy and prev_check_time to essentiallly 
612 # reset the hold.  If it is being activated, it runs the targeter in the background
613 sub update_hold_if_frozen {
614     my($self, $e, $hold, $orig_hold) = @_;
615     return if $hold->capture_time;
616
617     if($U->is_true($hold->frozen)) {
618         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
619         $hold->clear_current_copy;
620         $hold->clear_prev_check_time;
621
622     } else {
623         if($U->is_true($orig_hold->frozen)) {
624             $logger->info("Running targeter on activated hold ".$hold->id);
625                 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
626         }
627     }
628 }
629
630
631 __PACKAGE__->register_method(
632         method  => "retrieve_hold_status",
633         api_name        => "open-ils.circ.hold.status.retrieve",
634         notes           => <<"  NOTE");
635         Calculates the current status of the hold.
636         the requestor must have VIEW_HOLD permissions if the hold is for a user
637         other than the requestor.
638         Returns -1  on error (for now)
639         Returns 1 for 'waiting for copy to become available'
640         Returns 2 for 'waiting for copy capture'
641         Returns 3 for 'in transit'
642         Returns 4 for 'arrived'
643         NOTE
644
645 sub retrieve_hold_status {
646         my($self, $client, $auth, $hold_id) = @_;
647
648         my $e = new_editor(authtoken => $auth);
649         return $e->event unless $e->checkauth;
650         my $hold = $e->retrieve_action_hold_request($hold_id)
651                 or return $e->event;
652
653         if( $e->requestor->id != $hold->usr ) {
654                 return $e->event unless $e->allowed('VIEW_HOLD');
655         }
656
657         return _hold_status($e, $hold);
658
659 }
660
661 sub _hold_status {
662         my($e, $hold) = @_;
663         return 1 unless $hold->current_copy;
664         return 2 unless $hold->capture_time;
665
666         my $copy = $hold->current_copy;
667         unless( ref $copy ) {
668                 $copy = $e->retrieve_asset_copy($hold->current_copy)
669                         or return $e->event;
670         }
671
672         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
673         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
674
675         return -1;
676 }
677
678
679
680 __PACKAGE__->register_method(
681         method  => "retrieve_hold_queue_stats",
682         api_name        => "open-ils.circ.hold.queue_stats.retrieve",
683     signature => {
684         desc => q/
685             Returns object with total_holds count, queue_position, potential_copies count, and status code
686         /
687     }
688 );
689
690 sub retrieve_hold_queue_stats {
691     my($self, $conn, $auth, $hold_id) = @_;
692         my $e = new_editor(authtoken => $auth);
693         return $e->event unless $e->checkauth;
694         my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
695         if($e->requestor->id != $hold->usr) {
696                 return $e->event unless $e->allowed('VIEW_HOLD');
697         }
698     return retrieve_hold_queue_status_impl($e, $hold);
699 }
700
701 sub retrieve_hold_queue_status_impl {
702     my $e = shift;
703     my $hold = shift;
704
705     my $hold_ids = $e->search_action_hold_request(
706         [
707             {   target => $hold->target, 
708                 hold_type => $hold->hold_type,
709                 cancel_time => undef,
710                 capture_time => undef
711             },
712             {order_by => {ahr => 'request_time asc'}}
713         ], 
714         {idlist => 1} 
715     );
716
717     my $qpos = 0;
718     for my $hid (@$hold_ids) {
719         $qpos++;
720         last if $hid == $hold->id;
721     }
722
723     my $potentials = $e->search_action_hold_copy_map({hold => $hold->id}, {idlist => 1});
724     my $num_potentials = scalar(@$potentials);
725
726     my $user_org = $e->json_query({select => {au => 'home_ou'}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
727     my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
728     my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
729
730     return {
731         total_holds => scalar(@$hold_ids),
732         queue_position => $qpos,
733         potential_copies => $num_potentials,
734         status => _hold_status($e, $hold),
735         estimated_wait => int($estimated_wait)
736     };
737 }
738
739
740 sub fetch_open_hold_by_current_copy {
741         my $class = shift;
742         my $copyid = shift;
743         my $hold = $apputils->simplereq(
744                 'open-ils.cstore', 
745                 'open-ils.cstore.direct.action.hold_request.search.atomic',
746                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
747         return $hold->[0] if ref($hold);
748         return undef;
749 }
750
751 sub fetch_related_holds {
752         my $class = shift;
753         my $copyid = shift;
754         return $apputils->simplereq(
755                 'open-ils.cstore', 
756                 'open-ils.cstore.direct.action.hold_request.search.atomic',
757                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
758 }
759
760
761 __PACKAGE__->register_method (
762         method          => "hold_pull_list",
763         api_name                => "open-ils.circ.hold_pull_list.retrieve",
764         signature       => q/
765                 Returns a list of holds that need to be "pulled"
766                 by a given location
767         /
768 );
769
770 __PACKAGE__->register_method (
771         method          => "hold_pull_list",
772         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
773         signature       => q/
774                 Returns a list of hold ID's that need to be "pulled"
775                 by a given location
776         /
777 );
778
779
780 sub hold_pull_list {
781         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
782         my( $reqr, $evt ) = $U->checkses($authtoken);
783         return $evt if $evt;
784
785         my $org = $reqr->ws_ou || $reqr->home_ou;
786         # the perm locaiton shouldn't really matter here since holds
787         # will exist all over and VIEW_HOLDS should be universal
788         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
789         return $evt if $evt;
790
791         if( $self->api_name =~ /id_list/ ) {
792                 return $U->storagereq(
793                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
794                         $org, $limit, $offset ); 
795         } else {
796                 return $U->storagereq(
797                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
798                         $org, $limit, $offset ); 
799         }
800 }
801
802 __PACKAGE__->register_method (
803         method          => 'fetch_hold_notify',
804         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
805         signature       => q/ 
806                 Returns a list of hold notification objects based on hold id.
807                 @param authtoken The loggin session key
808                 @param holdid The id of the hold whose notifications we want to retrieve
809                 @return An array of hold notification objects, event on error.
810         /
811 );
812
813 sub fetch_hold_notify {
814         my( $self, $conn, $authtoken, $holdid ) = @_;
815         my( $requestor, $evt ) = $U->checkses($authtoken);
816         return $evt if $evt;
817         my ($hold, $patron);
818         ($hold, $evt) = $U->fetch_hold($holdid);
819         return $evt if $evt;
820         ($patron, $evt) = $U->fetch_user($hold->usr);
821         return $evt if $evt;
822
823         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
824         return $evt if $evt;
825
826         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
827         return $U->cstorereq(
828                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
829 }
830
831
832 __PACKAGE__->register_method (
833         method          => 'create_hold_notify',
834         api_name                => 'open-ils.circ.hold_notification.create',
835         signature       => q/
836                 Creates a new hold notification object
837                 @param authtoken The login session key
838                 @param notification The hold notification object to create
839                 @return ID of the new object on success, Event on error
840                 /
841 );
842
843 sub create_hold_notify {
844    my( $self, $conn, $auth, $note ) = @_;
845    my $e = new_editor(authtoken=>$auth, xact=>1);
846    return $e->die_event unless $e->checkauth;
847
848    my $hold = $e->retrieve_action_hold_request($note->hold)
849       or return $e->die_event;
850    my $patron = $e->retrieve_actor_user($hold->usr) 
851       or return $e->die_event;
852
853    return $e->die_event unless 
854       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
855
856         $note->notify_staff($e->requestor->id);
857    $e->create_action_hold_notification($note) or return $e->die_event;
858    $e->commit;
859    return $note->id;
860 }
861
862
863 __PACKAGE__->register_method(
864         method  => 'reset_hold',
865         api_name        => 'open-ils.circ.hold.reset',
866         signature       => q/
867                 Un-captures and un-targets a hold, essentially returning
868                 it to the state it was in directly after it was placed,
869                 then attempts to re-target the hold
870                 @param authtoken The login session key
871                 @param holdid The id of the hold
872         /
873 );
874
875
876 sub reset_hold {
877         my( $self, $conn, $auth, $holdid ) = @_;
878         my $reqr;
879         my ($hold, $evt) = $U->fetch_hold($holdid);
880         return $evt if $evt;
881         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
882         return $evt if $evt;
883         $evt = _reset_hold($self, $reqr, $hold);
884         return $evt if $evt;
885         return 1;
886 }
887
888 sub _reset_hold {
889         my ($self, $reqr, $hold) = @_;
890
891         my $e = new_editor(xact =>1, requestor => $reqr);
892
893         $logger->info("reseting hold ".$hold->id);
894
895         my $hid = $hold->id;
896
897         if( $hold->capture_time and $hold->current_copy ) {
898
899                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
900                         or return $e->event;
901
902                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
903                         $logger->info("setting copy to status 'reshelving' on hold retarget");
904                         $copy->status(OILS_COPY_STATUS_RESHELVING);
905                         $copy->editor($e->requestor->id);
906                         $copy->edit_date('now');
907                         $e->update_asset_copy($copy) or return $e->event;
908
909                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
910
911                         # We don't want the copy to remain "in transit"
912                         $copy->status(OILS_COPY_STATUS_RESHELVING);
913                         $logger->warn("! reseting hold [$hid] that is in transit");
914                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
915
916                         if( $transid ) {
917                                 my $trans = $e->retrieve_action_transit_copy($transid);
918                                 if( $trans ) {
919                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
920                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
921                                         $logger->info("Transit abort completed with result $evt");
922                                         return $evt unless "$evt" eq 1;
923                                 }
924                         }
925                 }
926         }
927
928         $hold->clear_capture_time;
929         $hold->clear_current_copy;
930
931         $e->update_action_hold_request($hold) or return $e->event;
932         $e->commit;
933
934         $U->storagereq(
935                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
936
937         return undef;
938 }
939
940
941 __PACKAGE__->register_method(
942         method => 'fetch_open_title_holds',
943         api_name        => 'open-ils.circ.open_holds.retrieve',
944         signature       => q/
945                 Returns a list ids of un-fulfilled holds for a given title id
946                 @param authtoken The login session key
947                 @param id the id of the item whose holds we want to retrieve
948                 @param type The hold type - M, T, V, C
949         /
950 );
951
952 sub fetch_open_title_holds {
953         my( $self, $conn, $auth, $id, $type, $org ) = @_;
954         my $e = new_editor( authtoken => $auth );
955         return $e->event unless $e->checkauth;
956
957         $type ||= "T";
958         $org ||= $e->requestor->ws_ou;
959
960 #       return $e->search_action_hold_request(
961 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
962
963         # XXX make me return IDs in the future ^--
964         my $holds = $e->search_action_hold_request(
965                 { 
966                         target                          => $id, 
967                         cancel_time                     => undef, 
968                         hold_type                       => $type, 
969                         fulfillment_time        => undef 
970                 }
971         );
972
973         flesh_hold_transits($holds);
974         return $holds;
975 }
976
977
978 sub flesh_hold_transits {
979         my $holds = shift;
980         for my $hold ( @$holds ) {
981                 $hold->transit(
982                         $apputils->simplereq(
983                                 'open-ils.cstore',
984                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
985                                 { hold => $hold->id },
986                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
987                         )->[0]
988                 );
989         }
990 }
991
992 sub flesh_hold_notices {
993         my( $holds, $e ) = @_;
994         $e ||= new_editor();
995
996         for my $hold (@$holds) {
997                 my $notices = $e->search_action_hold_notification(
998                         [
999                                 { hold => $hold->id },
1000                                 { order_by => { anh => 'notify_time desc' } },
1001                         ],
1002                         {idlist=>1}
1003                 );
1004
1005                 $hold->notify_count(scalar(@$notices));
1006                 if( @$notices ) {
1007                         my $n = $e->retrieve_action_hold_notification($$notices[0])
1008                                 or return $e->event;
1009                         $hold->notify_time($n->notify_time);
1010                 }
1011         }
1012 }
1013
1014
1015 __PACKAGE__->register_method(
1016         method => 'fetch_captured_holds',
1017         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1018     stream => 1,
1019         signature       => q/
1020                 Returns a list of un-fulfilled holds for a given title id
1021                 @param authtoken The login session key
1022                 @param org The org id of the location in question
1023         /
1024 );
1025
1026 __PACKAGE__->register_method(
1027         method => 'fetch_captured_holds',
1028         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1029     stream => 1,
1030         signature       => q/
1031                 Returns a list ids of un-fulfilled holds for a given title id
1032                 @param authtoken The login session key
1033                 @param org The org id of the location in question
1034         /
1035 );
1036
1037 sub fetch_captured_holds {
1038         my( $self, $conn, $auth, $org ) = @_;
1039
1040         my $e = new_editor(authtoken => $auth);
1041         return $e->event unless $e->checkauth;
1042         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1043
1044         $org ||= $e->requestor->ws_ou;
1045
1046     my $hold_ids = $e->json_query(
1047         { 
1048             select => { ahr => ['id'] },
1049             from => {
1050                 ahr => {
1051                     acp => {
1052                         field => 'id',
1053                         fkey => 'current_copy'
1054                     },
1055                 }
1056             }, 
1057             where => {
1058                 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1059                 '+ahr' => {
1060                     capture_time                => { "!=" => undef },
1061                     current_copy                => { "!=" => undef },
1062                     fulfillment_time    => undef,
1063                     pickup_lib                  => $org,
1064                     cancel_time                 => undef,
1065                 }
1066             }
1067         },
1068     );
1069
1070     for my $hold_id (@$hold_ids) {
1071         if($self->api_name =~ /id_list/) {
1072             $conn->respond($hold_id->{id});
1073             next;
1074         } else {
1075             $conn->respond(
1076                 $e->retrieve_action_hold_request([
1077                     $hold_id->{id},
1078                     {
1079                         flesh => 1,
1080                         flesh_fields => {ahr => ['notifications', 'transit']},
1081                         order_by => {anh => 'notify_time desc'}
1082                     }
1083                 ])
1084             );
1085         }
1086     }
1087
1088     return undef;
1089 }
1090 __PACKAGE__->register_method(
1091         method  => "check_title_hold",
1092         api_name        => "open-ils.circ.title_hold.is_possible",
1093         notes           => q/
1094                 Determines if a hold were to be placed by a given user,
1095                 whether or not said hold would have any potential copies
1096                 to fulfill it.
1097                 @param authtoken The login session key
1098                 @param params A hash of named params including:
1099                         patronid  - the id of the hold recipient
1100                         titleid (brn) - the id of the title to be held
1101                         depth   - the hold range depth (defaults to 0)
1102         /);
1103
1104 sub check_title_hold {
1105         my( $self, $client, $authtoken, $params ) = @_;
1106
1107         my %params              = %$params;
1108         my $titleid             = $params{titleid} ||"";
1109         my $volid               = $params{volume_id};
1110         my $copyid              = $params{copy_id};
1111         my $mrid                = $params{mrid} ||"";
1112         my $depth               = $params{depth} || 0;
1113         my $pickup_lib  = $params{pickup_lib};
1114         my $hold_type   = $params{hold_type} || 'T';
1115     my $selection_ou = $params{selection_ou} || $pickup_lib;
1116
1117         my $e = new_editor(authtoken=>$authtoken);
1118         return $e->event unless $e->checkauth;
1119         my $patron = $e->retrieve_actor_user($params{patronid})
1120                 or return $e->event;
1121
1122         if( $e->requestor->id ne $patron->id ) {
1123                 return $e->event unless 
1124                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1125         }
1126
1127         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1128
1129         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1130                 or return $e->event;
1131
1132     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1133     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1134
1135     if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1136         # work up the tree and as soon as we find a potential copy, use that depth
1137         # also, make sure we don't go past the hard boundary if it exists
1138
1139         # our min boundary is the greater of user-specified boundary or hard boundary
1140         my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?  
1141             $hard_boundary : $$params{depth};
1142
1143         my $depth = $soft_boundary;
1144         while($depth >= $min_depth) {
1145             $logger->info("performing hold possibility check with soft boundary $depth");
1146             return {success => 1, depth => $depth}
1147                 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1148             $depth--;
1149         }
1150         return {success => 0};
1151
1152     } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1153         # there is no soft boundary, enforce the hard boundary if it exists
1154         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1155         if(do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params)) {
1156             return {success => 1, depth => $hard_boundary}
1157         } else {
1158             return {success => 0};
1159         }
1160
1161     } else {
1162         # no boundaries defined, fall back to user specifed boundary or no boundary
1163         $logger->info("performing hold possibility check with no boundary");
1164         if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1165             return {success => 1, depth => $hard_boundary};
1166         } else {
1167             return {success => 0};
1168         }
1169     }
1170 }
1171
1172 sub do_possibility_checks {
1173     my($e, $patron, $request_lib, $depth, %params) = @_;
1174
1175         my $titleid             = $params{titleid} ||"";
1176         my $volid               = $params{volume_id};
1177         my $copyid              = $params{copy_id};
1178         my $mrid                = $params{mrid} ||"";
1179         my $pickup_lib  = $params{pickup_lib};
1180         my $hold_type   = $params{hold_type} || 'T';
1181     my $selection_ou = $params{selection_ou} || $pickup_lib;
1182
1183
1184         my $copy;
1185         my $volume;
1186         my $title;
1187
1188         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1189
1190                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1191                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1192                         or return $e->event;
1193                 $title = $e->retrieve_biblio_record_entry($volume->record)
1194                         or return $e->event;
1195                 return verify_copy_for_hold( 
1196                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1197
1198         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1199
1200                 $volume = $e->retrieve_asset_call_number($volid)
1201                         or return $e->event;
1202                 $title = $e->retrieve_biblio_record_entry($volume->record)
1203                         or return $e->event;
1204
1205                 return _check_volume_hold_is_possible(
1206                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1207
1208         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1209
1210                 return _check_title_hold_is_possible(
1211                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1212
1213         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1214
1215                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1216                 my @recs = map { $_->source } @$maps;
1217                 for my $rec (@recs) {
1218                         return 1 if (_check_title_hold_is_possible(
1219                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1220                 }
1221                 return 0;       
1222         }
1223 }
1224
1225 my %prox_cache;
1226
1227 sub _check_metarecord_hold_is_possible {
1228         my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1229    
1230    my $e = new_editor();
1231
1232     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1233     my $copies = $e->json_query(
1234         { 
1235             select => { acp => ['id', 'circ_lib'] },
1236             from => {
1237                 acp => {
1238                     acn => {
1239                         field => 'id',
1240                         fkey => 'call_number',
1241                         'join' => {
1242                             mmrsm => {
1243                                 field => 'source',
1244                                 fkey => 'record',
1245                                 filter => { metarecord => $mrid }
1246                             }
1247                         }
1248                     },
1249                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1250                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1251                 }
1252             }, 
1253             where => {
1254                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1255             }
1256         }
1257     );
1258
1259    return $e->event unless defined $copies;
1260    $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1261    return 0 unless @$copies;
1262
1263    # -----------------------------------------------------------------------
1264    # sort the copies into buckets based on their circ_lib proximity to 
1265    # the patron's home_ou.  
1266    # -----------------------------------------------------------------------
1267
1268    my $home_org = $patron->home_ou;
1269    my $req_org = $request_lib->id;
1270
1271     $prox_cache{$home_org} = 
1272         $e->search_actor_org_unit_proximity({from_org => $home_org})
1273         unless $prox_cache{$home_org};
1274     my $home_prox = $prox_cache{$home_org};
1275
1276    my %buckets;
1277    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1278    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1279
1280    my @keys = sort { $a <=> $b } keys %buckets;
1281
1282
1283    if( $home_org ne $req_org ) {
1284       # -----------------------------------------------------------------------
1285       # shove the copies close to the request_lib into the primary buckets 
1286       # directly before the farthest away copies.  That way, they are not 
1287       # given priority, but they are checked before the farthest copies.
1288       # -----------------------------------------------------------------------
1289
1290         $prox_cache{$req_org} = 
1291             $e->search_actor_org_unit_proximity({from_org => $req_org})
1292             unless $prox_cache{$req_org};
1293         my $req_prox = $prox_cache{$req_org};
1294
1295       my %buckets2;
1296       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1297       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1298
1299       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1300       my $new_key = $highest_key - 0.5; # right before the farthest prox
1301       my @keys2 = sort { $a <=> $b } keys %buckets2;
1302       for my $key (@keys2) {
1303          last if $key >= $highest_key;
1304          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1305       }
1306    }
1307
1308    @keys = sort { $a <=> $b } keys %buckets;
1309
1310    my %seen;
1311    for my $key (@keys) {
1312       my @cps = @{$buckets{$key}};
1313
1314       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1315
1316       for my $copyid (@cps) {
1317
1318          next if $seen{$copyid};
1319          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1320          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1321          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1322
1323          my $vol = $e->retrieve_asset_call_number(
1324            [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1325
1326          return 1 if verify_copy_for_hold( 
1327             $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1328    
1329       }
1330    }
1331
1332    return 0;
1333 }
1334
1335 sub create_ranged_org_filter {
1336     my($e, $selection_ou, $depth) = @_;
1337
1338     # find the orgs from which this hold may be fulfilled, 
1339     # based on the selection_ou and depth
1340
1341     my $top_org = $e->search_actor_org_unit([
1342         {parent_ou => undef}, 
1343         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1344     my %org_filter;
1345
1346     return () if $depth == $top_org->ou_type->depth;
1347
1348     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1349     %org_filter = (circ_lib => []);
1350     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1351
1352     $logger->info("hold org filter at depth $depth and selection_ou ".
1353         "$selection_ou created list of @{$org_filter{circ_lib}}");
1354
1355     return %org_filter;
1356 }
1357
1358
1359 sub _check_title_hold_is_possible {
1360         my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1361    
1362     my $e = new_editor();
1363     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1364
1365     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1366     my $copies = $e->json_query(
1367         { 
1368             select => { acp => ['id', 'circ_lib'] },
1369             from => {
1370                 acp => {
1371                     acn => {
1372                         field => 'id',
1373                         fkey => 'call_number',
1374                         'join' => {
1375                             bre => {
1376                                 field => 'id',
1377                                 filter => { id => $titleid },
1378                                 fkey => 'record'
1379                             }
1380                         }
1381                     },
1382                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1383                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1384                 }
1385             }, 
1386             where => {
1387                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1388             }
1389         }
1390     );
1391
1392    return $e->event unless defined $copies;
1393    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1394    return 0 unless @$copies;
1395
1396    # -----------------------------------------------------------------------
1397    # sort the copies into buckets based on their circ_lib proximity to 
1398    # the patron's home_ou.  
1399    # -----------------------------------------------------------------------
1400
1401    my $home_org = $patron->home_ou;
1402    my $req_org = $request_lib->id;
1403
1404     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1405
1406     $prox_cache{$home_org} = 
1407         $e->search_actor_org_unit_proximity({from_org => $home_org})
1408         unless $prox_cache{$home_org};
1409     my $home_prox = $prox_cache{$home_org};
1410
1411    my %buckets;
1412    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1413    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1414
1415    my @keys = sort { $a <=> $b } keys %buckets;
1416
1417
1418    if( $home_org ne $req_org ) {
1419       # -----------------------------------------------------------------------
1420       # shove the copies close to the request_lib into the primary buckets 
1421       # directly before the farthest away copies.  That way, they are not 
1422       # given priority, but they are checked before the farthest copies.
1423       # -----------------------------------------------------------------------
1424         $prox_cache{$req_org} = 
1425             $e->search_actor_org_unit_proximity({from_org => $req_org})
1426             unless $prox_cache{$req_org};
1427         my $req_prox = $prox_cache{$req_org};
1428
1429
1430       my %buckets2;
1431       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1432       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1433
1434       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1435       my $new_key = $highest_key - 0.5; # right before the farthest prox
1436       my @keys2 = sort { $a <=> $b } keys %buckets2;
1437       for my $key (@keys2) {
1438          last if $key >= $highest_key;
1439          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1440       }
1441    }
1442
1443    @keys = sort { $a <=> $b } keys %buckets;
1444
1445    my $title;
1446    my %seen;
1447    for my $key (@keys) {
1448       my @cps = @{$buckets{$key}};
1449
1450       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1451
1452       for my $copyid (@cps) {
1453
1454          next if $seen{$copyid};
1455          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1456          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1457          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1458
1459          unless($title) { # grab the title if we don't already have it
1460             my $vol = $e->retrieve_asset_call_number(
1461                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1462             $title = $vol->record;
1463          }
1464    
1465          return 1 if verify_copy_for_hold( 
1466             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1467    
1468       }
1469    }
1470
1471    return 0;
1472 }
1473
1474
1475 sub _check_volume_hold_is_possible {
1476         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1477     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1478         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1479         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1480         for my $copy ( @$copies ) {
1481                 return 1 if verify_copy_for_hold( 
1482                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1483         }
1484         return 0;
1485 }
1486
1487
1488
1489 sub verify_copy_for_hold {
1490         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1491         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1492         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1493                 {       patron                          => $patron, 
1494                         requestor                       => $requestor, 
1495                         copy                            => $copy,
1496                         title                           => $title, 
1497                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1498                         pickup_lib                      => $pickup_lib,
1499                         request_lib                     => $request_lib,
1500             new_hold            => 1
1501                 } 
1502         );
1503         return 0;
1504 }
1505
1506
1507
1508 sub find_nearest_permitted_hold {
1509
1510         my $class       = shift;
1511         my $editor      = shift; # CStoreEditor object
1512         my $copy                = shift; # copy to target
1513         my $user                = shift; # staff 
1514         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1515         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1516
1517         my $bc = $copy->barcode;
1518
1519         # find any existing holds that already target this copy
1520         my $old_holds = $editor->search_action_hold_request(
1521                 {       current_copy => $copy->id, 
1522                         cancel_time => undef, 
1523                         capture_time => undef 
1524                 } 
1525         );
1526
1527         # hold->type "R" means we need this copy
1528         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1529
1530
1531     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1532
1533         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1534         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1535
1536         # search for what should be the best holds for this copy to fulfill
1537         my $best_holds = $U->storagereq(
1538                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1539                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1540
1541         unless(@$best_holds) {
1542
1543                 if( my $hold = $$old_holds[0] ) {
1544                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1545                         return ($hold);
1546                 }
1547
1548                 $logger->info("circulator: no suitable holds found for copy $bc");
1549                 return (undef, $evt);
1550         }
1551
1552
1553         my $best_hold;
1554
1555         # for each potential hold, we have to run the permit script
1556         # to make sure the hold is actually permitted.
1557         for my $holdid (@$best_holds) {
1558                 next unless $holdid;
1559                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1560
1561                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1562                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1563                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1564
1565                 # see if this hold is permitted
1566                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1567                         {       patron_id                       => $hold->usr,
1568                                 requestor                       => $reqr,
1569                                 copy                            => $copy,
1570                                 pickup_lib                      => $hold->pickup_lib,
1571                                 request_lib                     => $rlib,
1572                         } 
1573                 );
1574
1575                 if( $permitted ) {
1576                         $best_hold = $hold;
1577                         last;
1578                 }
1579         }
1580
1581
1582         unless( $best_hold ) { # no "good" permitted holds were found
1583                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1584                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1585                         return ($hold);
1586                 }
1587
1588                 # we got nuthin
1589                 $logger->info("circulator: no suitable holds found for copy $bc");
1590                 return (undef, $evt);
1591         }
1592
1593         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1594
1595         # indicate a permitted hold was found
1596         return $best_hold if $check_only;
1597
1598         # we've found a permitted hold.  we need to "grab" the copy 
1599         # to prevent re-targeted holds (next part) from re-grabbing the copy
1600         $best_hold->current_copy($copy->id);
1601         $editor->update_action_hold_request($best_hold) 
1602                 or return (undef, $editor->event);
1603
1604
1605     my $retarget = 0;
1606
1607         # re-target any other holds that already target this copy
1608         for my $old_hold (@$old_holds) {
1609                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1610                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1611             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1612         $old_hold->clear_current_copy;
1613         $old_hold->clear_prev_check_time;
1614         $editor->update_action_hold_request($old_hold) 
1615             or return (undef, $editor->event);
1616         $retarget = 1;
1617         }
1618
1619         return ($best_hold, undef, $retarget);
1620 }
1621
1622
1623
1624
1625
1626
1627 __PACKAGE__->register_method(
1628         method => 'all_rec_holds',
1629         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1630 );
1631
1632 sub all_rec_holds {
1633         my( $self, $conn, $auth, $title_id, $args ) = @_;
1634
1635         my $e = new_editor(authtoken=>$auth);
1636         $e->checkauth or return $e->event;
1637         $e->allowed('VIEW_HOLD') or return $e->event;
1638
1639         $args ||= { fulfillment_time => undef };
1640         $args->{cancel_time} = undef;
1641
1642         my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1643
1644     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1645     if($mr_map) {
1646         $resp->{metarecord_holds} = $e->search_action_hold_request(
1647             {   hold_type => OILS_HOLD_TYPE_METARECORD,
1648                 target => $mr_map->metarecord,
1649                 %$args 
1650             }, {idlist => 1}
1651         );
1652     }
1653
1654         $resp->{title_holds} = $e->search_action_hold_request(
1655                 { 
1656                         hold_type => OILS_HOLD_TYPE_TITLE, 
1657                         target => $title_id, 
1658                         %$args 
1659                 }, {idlist=>1} );
1660
1661         my $vols = $e->search_asset_call_number(
1662                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1663
1664         return $resp unless @$vols;
1665
1666         $resp->{volume_holds} = $e->search_action_hold_request(
1667                 { 
1668                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1669                         target => $vols,
1670                         %$args }, 
1671                 {idlist=>1} );
1672
1673         my $copies = $e->search_asset_copy(
1674                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1675
1676         return $resp unless @$copies;
1677
1678         $resp->{copy_holds} = $e->search_action_hold_request(
1679                 { 
1680                         hold_type => OILS_HOLD_TYPE_COPY,
1681                         target => $copies,
1682                         %$args }, 
1683                 {idlist=>1} );
1684
1685         return $resp;
1686 }
1687
1688
1689
1690
1691
1692 __PACKAGE__->register_method(
1693         method => 'uber_hold',
1694     authoritative => 1,
1695         api_name => 'open-ils.circ.hold.details.retrieve'
1696 );
1697
1698 sub uber_hold {
1699         my($self, $client, $auth, $hold_id) = @_;
1700         my $e = new_editor(authtoken=>$auth);
1701         $e->checkauth or return $e->event;
1702         $e->allowed('VIEW_HOLD') or return $e->event;
1703
1704         my $resp = {};
1705
1706         my $hold = $e->retrieve_action_hold_request(
1707                 [
1708                         $hold_id,
1709                         {
1710                                 flesh => 1,
1711                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1712                         }
1713                 ]
1714         ) or return $e->event;
1715
1716         my $user = $hold->usr;
1717         $hold->usr($user->id);
1718
1719         my $card = $e->retrieve_actor_card($user->card)
1720                 or return $e->event;
1721
1722         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1723
1724         flesh_hold_notices([$hold], $e);
1725         flesh_hold_transits([$hold]);
1726
1727         return {
1728                 hold            => $hold,
1729                 copy            => $copy,
1730                 volume  => $volume,
1731                 mvr             => $mvr,
1732                 status  => _hold_status($e, $hold),
1733                 patron_first => $user->first_given_name,
1734                 patron_last  => $user->family_name,
1735                 patron_barcode => $card->barcode,
1736         };
1737 }
1738
1739
1740
1741 # -----------------------------------------------------
1742 # Returns the MVR object that represents what the
1743 # hold is all about
1744 # -----------------------------------------------------
1745 sub find_hold_mvr {
1746         my( $e, $hold ) = @_;
1747
1748         my $tid;
1749         my $copy;
1750         my $volume;
1751
1752         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1753                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1754                         or return $e->event;
1755                 $tid = $mr->master_record;
1756
1757         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1758                 $tid = $hold->target;
1759
1760         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1761                 $volume = $e->retrieve_asset_call_number($hold->target)
1762                         or return $e->event;
1763                 $tid = $volume->record;
1764
1765         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1766                 $copy = $e->retrieve_asset_copy($hold->target)
1767                         or return $e->event;
1768                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1769                         or return $e->event;
1770                 $tid = $volume->record;
1771         }
1772
1773         if(!$copy and ref $hold->current_copy ) {
1774                 $copy = $hold->current_copy;
1775                 $hold->current_copy($copy->id);
1776         }
1777
1778         if(!$volume and $copy) {
1779                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1780         }
1781
1782         my $title = $e->retrieve_biblio_record_entry($tid);
1783         return ( $U->record_to_mvr($title), $volume, $copy );
1784 }
1785
1786
1787
1788
1789 1;