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