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