]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
return hold status with queue stats
[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, potential_copies count, and status code
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         status => _hold_status($e, $hold)
708     };
709 }
710
711
712 sub fetch_open_hold_by_current_copy {
713         my $class = shift;
714         my $copyid = shift;
715         my $hold = $apputils->simplereq(
716                 'open-ils.cstore', 
717                 'open-ils.cstore.direct.action.hold_request.search.atomic',
718                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
719         return $hold->[0] if ref($hold);
720         return undef;
721 }
722
723 sub fetch_related_holds {
724         my $class = shift;
725         my $copyid = shift;
726         return $apputils->simplereq(
727                 'open-ils.cstore', 
728                 'open-ils.cstore.direct.action.hold_request.search.atomic',
729                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
730 }
731
732
733 __PACKAGE__->register_method (
734         method          => "hold_pull_list",
735         api_name                => "open-ils.circ.hold_pull_list.retrieve",
736         signature       => q/
737                 Returns a list of holds that need to be "pulled"
738                 by a given location
739         /
740 );
741
742 __PACKAGE__->register_method (
743         method          => "hold_pull_list",
744         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
745         signature       => q/
746                 Returns a list of hold ID's that need to be "pulled"
747                 by a given location
748         /
749 );
750
751
752 sub hold_pull_list {
753         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
754         my( $reqr, $evt ) = $U->checkses($authtoken);
755         return $evt if $evt;
756
757         my $org = $reqr->ws_ou || $reqr->home_ou;
758         # the perm locaiton shouldn't really matter here since holds
759         # will exist all over and VIEW_HOLDS should be universal
760         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
761         return $evt if $evt;
762
763         if( $self->api_name =~ /id_list/ ) {
764                 return $U->storagereq(
765                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
766                         $org, $limit, $offset ); 
767         } else {
768                 return $U->storagereq(
769                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
770                         $org, $limit, $offset ); 
771         }
772 }
773
774 __PACKAGE__->register_method (
775         method          => 'fetch_hold_notify',
776         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
777         signature       => q/ 
778                 Returns a list of hold notification objects based on hold id.
779                 @param authtoken The loggin session key
780                 @param holdid The id of the hold whose notifications we want to retrieve
781                 @return An array of hold notification objects, event on error.
782         /
783 );
784
785 sub fetch_hold_notify {
786         my( $self, $conn, $authtoken, $holdid ) = @_;
787         my( $requestor, $evt ) = $U->checkses($authtoken);
788         return $evt if $evt;
789         my ($hold, $patron);
790         ($hold, $evt) = $U->fetch_hold($holdid);
791         return $evt if $evt;
792         ($patron, $evt) = $U->fetch_user($hold->usr);
793         return $evt if $evt;
794
795         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
796         return $evt if $evt;
797
798         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
799         return $U->cstorereq(
800                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
801 }
802
803
804 __PACKAGE__->register_method (
805         method          => 'create_hold_notify',
806         api_name                => 'open-ils.circ.hold_notification.create',
807         signature       => q/
808                 Creates a new hold notification object
809                 @param authtoken The login session key
810                 @param notification The hold notification object to create
811                 @return ID of the new object on success, Event on error
812                 /
813 );
814
815 sub create_hold_notify {
816    my( $self, $conn, $auth, $note ) = @_;
817    my $e = new_editor(authtoken=>$auth, xact=>1);
818    return $e->die_event unless $e->checkauth;
819
820    my $hold = $e->retrieve_action_hold_request($note->hold)
821       or return $e->die_event;
822    my $patron = $e->retrieve_actor_user($hold->usr) 
823       or return $e->die_event;
824
825    return $e->die_event unless 
826       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
827
828         $note->notify_staff($e->requestor->id);
829    $e->create_action_hold_notification($note) or return $e->die_event;
830    $e->commit;
831    return $note->id;
832 }
833
834
835 __PACKAGE__->register_method(
836         method  => 'reset_hold',
837         api_name        => 'open-ils.circ.hold.reset',
838         signature       => q/
839                 Un-captures and un-targets a hold, essentially returning
840                 it to the state it was in directly after it was placed,
841                 then attempts to re-target the hold
842                 @param authtoken The login session key
843                 @param holdid The id of the hold
844         /
845 );
846
847
848 sub reset_hold {
849         my( $self, $conn, $auth, $holdid ) = @_;
850         my $reqr;
851         my ($hold, $evt) = $U->fetch_hold($holdid);
852         return $evt if $evt;
853         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
854         return $evt if $evt;
855         $evt = _reset_hold($self, $reqr, $hold);
856         return $evt if $evt;
857         return 1;
858 }
859
860 sub _reset_hold {
861         my ($self, $reqr, $hold) = @_;
862
863         my $e = new_editor(xact =>1, requestor => $reqr);
864
865         $logger->info("reseting hold ".$hold->id);
866
867         my $hid = $hold->id;
868
869         if( $hold->capture_time and $hold->current_copy ) {
870
871                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
872                         or return $e->event;
873
874                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
875                         $logger->info("setting copy to status 'reshelving' on hold retarget");
876                         $copy->status(OILS_COPY_STATUS_RESHELVING);
877                         $copy->editor($e->requestor->id);
878                         $copy->edit_date('now');
879                         $e->update_asset_copy($copy) or return $e->event;
880
881                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
882
883                         # We don't want the copy to remain "in transit"
884                         $copy->status(OILS_COPY_STATUS_RESHELVING);
885                         $logger->warn("! reseting hold [$hid] that is in transit");
886                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
887
888                         if( $transid ) {
889                                 my $trans = $e->retrieve_action_transit_copy($transid);
890                                 if( $trans ) {
891                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
892                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
893                                         $logger->info("Transit abort completed with result $evt");
894                                         return $evt unless "$evt" eq 1;
895                                 }
896                         }
897                 }
898         }
899
900         $hold->clear_capture_time;
901         $hold->clear_current_copy;
902
903         $e->update_action_hold_request($hold) or return $e->event;
904         $e->commit;
905
906         $U->storagereq(
907                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
908
909         return undef;
910 }
911
912
913 __PACKAGE__->register_method(
914         method => 'fetch_open_title_holds',
915         api_name        => 'open-ils.circ.open_holds.retrieve',
916         signature       => q/
917                 Returns a list ids of un-fulfilled holds for a given title id
918                 @param authtoken The login session key
919                 @param id the id of the item whose holds we want to retrieve
920                 @param type The hold type - M, T, V, C
921         /
922 );
923
924 sub fetch_open_title_holds {
925         my( $self, $conn, $auth, $id, $type, $org ) = @_;
926         my $e = new_editor( authtoken => $auth );
927         return $e->event unless $e->checkauth;
928
929         $type ||= "T";
930         $org ||= $e->requestor->ws_ou;
931
932 #       return $e->search_action_hold_request(
933 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
934
935         # XXX make me return IDs in the future ^--
936         my $holds = $e->search_action_hold_request(
937                 { 
938                         target                          => $id, 
939                         cancel_time                     => undef, 
940                         hold_type                       => $type, 
941                         fulfillment_time        => undef 
942                 }
943         );
944
945         flesh_hold_transits($holds);
946         return $holds;
947 }
948
949
950 sub flesh_hold_transits {
951         my $holds = shift;
952         for my $hold ( @$holds ) {
953                 $hold->transit(
954                         $apputils->simplereq(
955                                 'open-ils.cstore',
956                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
957                                 { hold => $hold->id },
958                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
959                         )->[0]
960                 );
961         }
962 }
963
964 sub flesh_hold_notices {
965         my( $holds, $e ) = @_;
966         $e ||= new_editor();
967
968         for my $hold (@$holds) {
969                 my $notices = $e->search_action_hold_notification(
970                         [
971                                 { hold => $hold->id },
972                                 { order_by => { anh => 'notify_time desc' } },
973                         ],
974                         {idlist=>1}
975                 );
976
977                 $hold->notify_count(scalar(@$notices));
978                 if( @$notices ) {
979                         my $n = $e->retrieve_action_hold_notification($$notices[0])
980                                 or return $e->event;
981                         $hold->notify_time($n->notify_time);
982                 }
983         }
984 }
985
986
987
988
989 __PACKAGE__->register_method(
990         method => 'fetch_captured_holds',
991         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
992         signature       => q/
993                 Returns a list of un-fulfilled holds for a given title id
994                 @param authtoken The login session key
995                 @param org The org id of the location in question
996         /
997 );
998
999 __PACKAGE__->register_method(
1000         method => 'fetch_captured_holds',
1001         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1002         signature       => q/
1003                 Returns a list ids of un-fulfilled holds for a given title id
1004                 @param authtoken The login session key
1005                 @param org The org id of the location in question
1006         /
1007 );
1008
1009 sub fetch_captured_holds {
1010         my( $self, $conn, $auth, $org ) = @_;
1011
1012         my $e = new_editor(authtoken => $auth);
1013         return $e->event unless $e->checkauth;
1014         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1015
1016         $org ||= $e->requestor->ws_ou;
1017
1018         my $holds = $e->search_action_hold_request(
1019                 { 
1020                         capture_time            => { "!=" => undef },
1021                         current_copy            => { "!=" => undef },
1022                         fulfillment_time        => undef,
1023                         pickup_lib                      => $org,
1024                         cancel_time                     => undef,
1025                 }
1026         );
1027
1028         my @res;
1029         for my $h (@$holds) {
1030                 my $copy = $e->retrieve_asset_copy($h->current_copy)
1031                         or return $e->event;
1032                 push( @res, $h ) if 
1033                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1034         }
1035
1036         if( ! $self->api_name =~ /id_list/ ) {
1037                 flesh_hold_transits(\@res);
1038                 flesh_hold_notices(\@res, $e);
1039         }
1040
1041         if( $self->api_name =~ /id_list/ ) {
1042                 return [ map { $_->id } @res ];
1043         } else {
1044                 return \@res;
1045         }
1046 }
1047
1048 __PACKAGE__->register_method(
1049         method  => "check_title_hold",
1050         api_name        => "open-ils.circ.title_hold.is_possible",
1051         notes           => q/
1052                 Determines if a hold were to be placed by a given user,
1053                 whether or not said hold would have any potential copies
1054                 to fulfill it.
1055                 @param authtoken The login session key
1056                 @param params A hash of named params including:
1057                         patronid  - the id of the hold recipient
1058                         titleid (brn) - the id of the title to be held
1059                         depth   - the hold range depth (defaults to 0)
1060         /);
1061
1062 sub check_title_hold {
1063         my( $self, $client, $authtoken, $params ) = @_;
1064
1065         my %params              = %$params;
1066         my $titleid             = $params{titleid} ||"";
1067         my $volid               = $params{volume_id};
1068         my $copyid              = $params{copy_id};
1069         my $mrid                = $params{mrid} ||"";
1070         my $depth               = $params{depth} || 0;
1071         my $pickup_lib  = $params{pickup_lib};
1072         my $hold_type   = $params{hold_type} || 'T';
1073     my $selection_ou = $params{selection_ou} || $pickup_lib;
1074
1075         my $e = new_editor(authtoken=>$authtoken);
1076         return $e->event unless $e->checkauth;
1077         my $patron = $e->retrieve_actor_user($params{patronid})
1078                 or return $e->event;
1079
1080         if( $e->requestor->id ne $patron->id ) {
1081                 return $e->event unless 
1082                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1083         }
1084
1085         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1086
1087         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1088                 or return $e->event;
1089
1090     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1091     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1092
1093     if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1094         # work up the tree and as soon as we find a potential copy, use that depth
1095         # also, make sure we don't go past the hard boundary if it exists
1096
1097         # our min boundary is the greater of user-specified boundary or hard boundary
1098         my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?  
1099             $hard_boundary : $$params{depth};
1100
1101         my $depth = $soft_boundary;
1102         while($depth >= $min_depth) {
1103             $logger->info("performing hold possibility check with soft boundary $depth");
1104             return {success => 1, depth => $depth}
1105                 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1106             $depth--;
1107         }
1108         return {success => 0};
1109
1110     } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1111         # there is no soft boundary, enforce the hard boundary if it exists
1112         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1113         if(do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params)) {
1114             return {success => 1, depth => $hard_boundary}
1115         } else {
1116             return {success => 0};
1117         }
1118
1119     } else {
1120         # no boundaries defined, fall back to user specifed boundary or no boundary
1121         $logger->info("performing hold possibility check with no boundary");
1122         if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1123             return {success => 1, depth => $hard_boundary};
1124         } else {
1125             return {success => 0};
1126         }
1127     }
1128 }
1129
1130 sub do_possibility_checks {
1131     my($e, $patron, $request_lib, $depth, %params) = @_;
1132
1133         my $titleid             = $params{titleid} ||"";
1134         my $volid               = $params{volume_id};
1135         my $copyid              = $params{copy_id};
1136         my $mrid                = $params{mrid} ||"";
1137         my $pickup_lib  = $params{pickup_lib};
1138         my $hold_type   = $params{hold_type} || 'T';
1139     my $selection_ou = $params{selection_ou} || $pickup_lib;
1140
1141
1142         my $copy;
1143         my $volume;
1144         my $title;
1145
1146         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1147
1148                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1149                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1150                         or return $e->event;
1151                 $title = $e->retrieve_biblio_record_entry($volume->record)
1152                         or return $e->event;
1153                 return verify_copy_for_hold( 
1154                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1155
1156         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1157
1158                 $volume = $e->retrieve_asset_call_number($volid)
1159                         or return $e->event;
1160                 $title = $e->retrieve_biblio_record_entry($volume->record)
1161                         or return $e->event;
1162
1163                 return _check_volume_hold_is_possible(
1164                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1165
1166         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1167
1168                 return _check_title_hold_is_possible(
1169                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1170
1171         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1172
1173                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1174                 my @recs = map { $_->source } @$maps;
1175                 for my $rec (@recs) {
1176                         return 1 if (_check_title_hold_is_possible(
1177                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1178                 }
1179                 return 0;       
1180         }
1181 }
1182
1183 my %prox_cache;
1184
1185 sub _check_metarecord_hold_is_possible {
1186         my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1187    
1188    my $e = new_editor();
1189
1190     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1191     my $copies = $e->json_query(
1192         { 
1193             select => { acp => ['id', 'circ_lib'] },
1194             from => {
1195                 acp => {
1196                     acn => {
1197                         field => 'id',
1198                         fkey => 'call_number',
1199                         'join' => {
1200                             mmrsm => {
1201                                 field => 'source',
1202                                 fkey => 'record',
1203                                 filter => { metarecord => $mrid }
1204                             }
1205                         }
1206                     },
1207                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1208                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1209                 }
1210             }, 
1211             where => {
1212                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1213             }
1214         }
1215     );
1216
1217    return $e->event unless defined $copies;
1218    $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1219    return 0 unless @$copies;
1220
1221    # -----------------------------------------------------------------------
1222    # sort the copies into buckets based on their circ_lib proximity to 
1223    # the patron's home_ou.  
1224    # -----------------------------------------------------------------------
1225
1226    my $home_org = $patron->home_ou;
1227    my $req_org = $request_lib->id;
1228
1229     $prox_cache{$home_org} = 
1230         $e->search_actor_org_unit_proximity({from_org => $home_org})
1231         unless $prox_cache{$home_org};
1232     my $home_prox = $prox_cache{$home_org};
1233
1234    my %buckets;
1235    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1236    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1237
1238    my @keys = sort { $a <=> $b } keys %buckets;
1239
1240
1241    if( $home_org ne $req_org ) {
1242       # -----------------------------------------------------------------------
1243       # shove the copies close to the request_lib into the primary buckets 
1244       # directly before the farthest away copies.  That way, they are not 
1245       # given priority, but they are checked before the farthest copies.
1246       # -----------------------------------------------------------------------
1247
1248         $prox_cache{$req_org} = 
1249             $e->search_actor_org_unit_proximity({from_org => $req_org})
1250             unless $prox_cache{$req_org};
1251         my $req_prox = $prox_cache{$req_org};
1252
1253       my %buckets2;
1254       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1255       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1256
1257       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1258       my $new_key = $highest_key - 0.5; # right before the farthest prox
1259       my @keys2 = sort { $a <=> $b } keys %buckets2;
1260       for my $key (@keys2) {
1261          last if $key >= $highest_key;
1262          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1263       }
1264    }
1265
1266    @keys = sort { $a <=> $b } keys %buckets;
1267
1268    my %seen;
1269    for my $key (@keys) {
1270       my @cps = @{$buckets{$key}};
1271
1272       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1273
1274       for my $copyid (@cps) {
1275
1276          next if $seen{$copyid};
1277          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1278          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1279          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1280
1281          my $vol = $e->retrieve_asset_call_number(
1282            [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1283
1284          return 1 if verify_copy_for_hold( 
1285             $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1286    
1287       }
1288    }
1289
1290    return 0;
1291 }
1292
1293 sub create_ranged_org_filter {
1294     my($e, $selection_ou, $depth) = @_;
1295
1296     # find the orgs from which this hold may be fulfilled, 
1297     # based on the selection_ou and depth
1298
1299     my $top_org = $e->search_actor_org_unit([
1300         {parent_ou => undef}, 
1301         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1302     my %org_filter;
1303
1304     return () if $depth == $top_org->ou_type->depth;
1305
1306     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1307     %org_filter = (circ_lib => []);
1308     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1309
1310     $logger->info("hold org filter at depth $depth and selection_ou ".
1311         "$selection_ou created list of @{$org_filter{circ_lib}}");
1312
1313     return %org_filter;
1314 }
1315
1316
1317 sub _check_title_hold_is_possible {
1318         my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1319    
1320     my $e = new_editor();
1321     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1322
1323     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1324     my $copies = $e->json_query(
1325         { 
1326             select => { acp => ['id', 'circ_lib'] },
1327             from => {
1328                 acp => {
1329                     acn => {
1330                         field => 'id',
1331                         fkey => 'call_number',
1332                         'join' => {
1333                             bre => {
1334                                 field => 'id',
1335                                 filter => { id => $titleid },
1336                                 fkey => 'record'
1337                             }
1338                         }
1339                     },
1340                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1341                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1342                 }
1343             }, 
1344             where => {
1345                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1346             }
1347         }
1348     );
1349
1350    return $e->event unless defined $copies;
1351    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1352    return 0 unless @$copies;
1353
1354    # -----------------------------------------------------------------------
1355    # sort the copies into buckets based on their circ_lib proximity to 
1356    # the patron's home_ou.  
1357    # -----------------------------------------------------------------------
1358
1359    my $home_org = $patron->home_ou;
1360    my $req_org = $request_lib->id;
1361
1362     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1363
1364     $prox_cache{$home_org} = 
1365         $e->search_actor_org_unit_proximity({from_org => $home_org})
1366         unless $prox_cache{$home_org};
1367     my $home_prox = $prox_cache{$home_org};
1368
1369    my %buckets;
1370    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1371    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1372
1373    my @keys = sort { $a <=> $b } keys %buckets;
1374
1375
1376    if( $home_org ne $req_org ) {
1377       # -----------------------------------------------------------------------
1378       # shove the copies close to the request_lib into the primary buckets 
1379       # directly before the farthest away copies.  That way, they are not 
1380       # given priority, but they are checked before the farthest copies.
1381       # -----------------------------------------------------------------------
1382         $prox_cache{$req_org} = 
1383             $e->search_actor_org_unit_proximity({from_org => $req_org})
1384             unless $prox_cache{$req_org};
1385         my $req_prox = $prox_cache{$req_org};
1386
1387
1388       my %buckets2;
1389       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1390       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1391
1392       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1393       my $new_key = $highest_key - 0.5; # right before the farthest prox
1394       my @keys2 = sort { $a <=> $b } keys %buckets2;
1395       for my $key (@keys2) {
1396          last if $key >= $highest_key;
1397          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1398       }
1399    }
1400
1401    @keys = sort { $a <=> $b } keys %buckets;
1402
1403    my $title;
1404    my %seen;
1405    for my $key (@keys) {
1406       my @cps = @{$buckets{$key}};
1407
1408       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1409
1410       for my $copyid (@cps) {
1411
1412          next if $seen{$copyid};
1413          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1414          my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1415          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1416
1417          unless($title) { # grab the title if we don't already have it
1418             my $vol = $e->retrieve_asset_call_number(
1419                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1420             $title = $vol->record;
1421          }
1422    
1423          return 1 if verify_copy_for_hold( 
1424             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1425    
1426       }
1427    }
1428
1429    return 0;
1430 }
1431
1432
1433 sub _check_volume_hold_is_possible {
1434         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1435     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1436         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1437         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1438         for my $copy ( @$copies ) {
1439                 return 1 if verify_copy_for_hold( 
1440                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1441         }
1442         return 0;
1443 }
1444
1445
1446
1447 sub verify_copy_for_hold {
1448         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1449         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1450         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1451                 {       patron                          => $patron, 
1452                         requestor                       => $requestor, 
1453                         copy                            => $copy,
1454                         title                           => $title, 
1455                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1456                         pickup_lib                      => $pickup_lib,
1457                         request_lib                     => $request_lib,
1458             new_hold            => 1
1459                 } 
1460         );
1461         return 0;
1462 }
1463
1464
1465
1466 sub find_nearest_permitted_hold {
1467
1468         my $class       = shift;
1469         my $editor      = shift; # CStoreEditor object
1470         my $copy                = shift; # copy to target
1471         my $user                = shift; # staff 
1472         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1473         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1474
1475         my $bc = $copy->barcode;
1476
1477         # find any existing holds that already target this copy
1478         my $old_holds = $editor->search_action_hold_request(
1479                 {       current_copy => $copy->id, 
1480                         cancel_time => undef, 
1481                         capture_time => undef 
1482                 } 
1483         );
1484
1485         # hold->type "R" means we need this copy
1486         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1487
1488
1489     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1490
1491         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1492         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1493
1494         # search for what should be the best holds for this copy to fulfill
1495         my $best_holds = $U->storagereq(
1496                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1497                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1498
1499         unless(@$best_holds) {
1500
1501                 if( my $hold = $$old_holds[0] ) {
1502                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1503                         return ($hold);
1504                 }
1505
1506                 $logger->info("circulator: no suitable holds found for copy $bc");
1507                 return (undef, $evt);
1508         }
1509
1510
1511         my $best_hold;
1512
1513         # for each potential hold, we have to run the permit script
1514         # to make sure the hold is actually permitted.
1515         for my $holdid (@$best_holds) {
1516                 next unless $holdid;
1517                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1518
1519                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1520                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1521                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1522
1523                 # see if this hold is permitted
1524                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1525                         {       patron_id                       => $hold->usr,
1526                                 requestor                       => $reqr,
1527                                 copy                            => $copy,
1528                                 pickup_lib                      => $hold->pickup_lib,
1529                                 request_lib                     => $rlib,
1530                         } 
1531                 );
1532
1533                 if( $permitted ) {
1534                         $best_hold = $hold;
1535                         last;
1536                 }
1537         }
1538
1539
1540         unless( $best_hold ) { # no "good" permitted holds were found
1541                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1542                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1543                         return ($hold);
1544                 }
1545
1546                 # we got nuthin
1547                 $logger->info("circulator: no suitable holds found for copy $bc");
1548                 return (undef, $evt);
1549         }
1550
1551         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1552
1553         # indicate a permitted hold was found
1554         return $best_hold if $check_only;
1555
1556         # we've found a permitted hold.  we need to "grab" the copy 
1557         # to prevent re-targeted holds (next part) from re-grabbing the copy
1558         $best_hold->current_copy($copy->id);
1559         $editor->update_action_hold_request($best_hold) 
1560                 or return (undef, $editor->event);
1561
1562
1563     my $retarget = 0;
1564
1565         # re-target any other holds that already target this copy
1566         for my $old_hold (@$old_holds) {
1567                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1568                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1569             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1570         $old_hold->clear_current_copy;
1571         $old_hold->clear_prev_check_time;
1572         $editor->update_action_hold_request($old_hold) 
1573             or return (undef, $editor->event);
1574         $retarget = 1;
1575         }
1576
1577         return ($best_hold, undef, $retarget);
1578 }
1579
1580
1581
1582
1583
1584
1585 __PACKAGE__->register_method(
1586         method => 'all_rec_holds',
1587         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1588 );
1589
1590 sub all_rec_holds {
1591         my( $self, $conn, $auth, $title_id, $args ) = @_;
1592
1593         my $e = new_editor(authtoken=>$auth);
1594         $e->checkauth or return $e->event;
1595         $e->allowed('VIEW_HOLD') or return $e->event;
1596
1597         $args ||= { fulfillment_time => undef };
1598         $args->{cancel_time} = undef;
1599
1600         my $resp = { volume_holds => [], copy_holds => [] };
1601
1602         $resp->{title_holds} = $e->search_action_hold_request(
1603                 { 
1604                         hold_type => OILS_HOLD_TYPE_TITLE, 
1605                         target => $title_id, 
1606                         %$args 
1607                 }, {idlist=>1} );
1608
1609         my $vols = $e->search_asset_call_number(
1610                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1611
1612         return $resp unless @$vols;
1613
1614         $resp->{volume_holds} = $e->search_action_hold_request(
1615                 { 
1616                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1617                         target => $vols,
1618                         %$args }, 
1619                 {idlist=>1} );
1620
1621         my $copies = $e->search_asset_copy(
1622                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1623
1624         return $resp unless @$copies;
1625
1626         $resp->{copy_holds} = $e->search_action_hold_request(
1627                 { 
1628                         hold_type => OILS_HOLD_TYPE_COPY,
1629                         target => $copies,
1630                         %$args }, 
1631                 {idlist=>1} );
1632
1633         return $resp;
1634 }
1635
1636
1637
1638
1639
1640 __PACKAGE__->register_method(
1641         method => 'uber_hold',
1642     authoritative => 1,
1643         api_name => 'open-ils.circ.hold.details.retrieve'
1644 );
1645
1646 sub uber_hold {
1647         my($self, $client, $auth, $hold_id) = @_;
1648         my $e = new_editor(authtoken=>$auth);
1649         $e->checkauth or return $e->event;
1650         $e->allowed('VIEW_HOLD') or return $e->event;
1651
1652         my $resp = {};
1653
1654         my $hold = $e->retrieve_action_hold_request(
1655                 [
1656                         $hold_id,
1657                         {
1658                                 flesh => 1,
1659                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1660                         }
1661                 ]
1662         ) or return $e->event;
1663
1664         my $user = $hold->usr;
1665         $hold->usr($user->id);
1666
1667         my $card = $e->retrieve_actor_card($user->card)
1668                 or return $e->event;
1669
1670         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1671
1672         flesh_hold_notices([$hold], $e);
1673         flesh_hold_transits([$hold]);
1674
1675         return {
1676                 hold            => $hold,
1677                 copy            => $copy,
1678                 volume  => $volume,
1679                 mvr             => $mvr,
1680                 status  => _hold_status($e, $hold),
1681                 patron_first => $user->first_given_name,
1682                 patron_last  => $user->family_name,
1683                 patron_barcode => $card->barcode,
1684         };
1685 }
1686
1687
1688
1689 # -----------------------------------------------------
1690 # Returns the MVR object that represents what the
1691 # hold is all about
1692 # -----------------------------------------------------
1693 sub find_hold_mvr {
1694         my( $e, $hold ) = @_;
1695
1696         my $tid;
1697         my $copy;
1698         my $volume;
1699
1700         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1701                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1702                         or return $e->event;
1703                 $tid = $mr->master_record;
1704
1705         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1706                 $tid = $hold->target;
1707
1708         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1709                 $volume = $e->retrieve_asset_call_number($hold->target)
1710                         or return $e->event;
1711                 $tid = $volume->record;
1712
1713         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1714                 $copy = $e->retrieve_asset_copy($hold->target)
1715                         or return $e->event;
1716                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1717                         or return $e->event;
1718                 $tid = $volume->record;
1719         }
1720
1721         if(!$copy and ref $hold->current_copy ) {
1722                 $copy = $hold->current_copy;
1723                 $hold->current_copy($copy->id);
1724         }
1725
1726         if(!$volume and $copy) {
1727                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1728         }
1729
1730         my $title = $e->retrieve_biblio_record_entry($tid);
1731         return ( $U->record_to_mvr($title), $volume, $copy );
1732 }
1733
1734
1735
1736
1737 1;