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