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