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