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