]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
Fix Hold Note creation in staff client by replacing permacrud call with a cstore...
[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     authoritative => 1,
904         signature       => q/ 
905                 Returns a list of hold notification objects based on hold id.
906                 @param authtoken The loggin session key
907                 @param holdid The id of the hold whose notifications we want to retrieve
908                 @return An array of hold notification objects, event on error.
909         /
910 );
911
912 sub fetch_hold_notify {
913         my( $self, $conn, $authtoken, $holdid ) = @_;
914         my( $requestor, $evt ) = $U->checkses($authtoken);
915         return $evt if $evt;
916         my ($hold, $patron);
917         ($hold, $evt) = $U->fetch_hold($holdid);
918         return $evt if $evt;
919         ($patron, $evt) = $U->fetch_user($hold->usr);
920         return $evt if $evt;
921
922         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
923         return $evt if $evt;
924
925         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
926         return $U->cstorereq(
927                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
928 }
929
930
931 __PACKAGE__->register_method (
932         method          => 'create_hold_notify',
933         api_name                => 'open-ils.circ.hold_notification.create',
934         signature       => q/
935                 Creates a new hold notification object
936                 @param authtoken The login session key
937                 @param notification The hold notification object to create
938                 @return ID of the new object on success, Event on error
939                 /
940 );
941
942 sub create_hold_notify {
943    my( $self, $conn, $auth, $note ) = @_;
944    my $e = new_editor(authtoken=>$auth, xact=>1);
945    return $e->die_event unless $e->checkauth;
946
947    my $hold = $e->retrieve_action_hold_request($note->hold)
948       or return $e->die_event;
949    my $patron = $e->retrieve_actor_user($hold->usr) 
950       or return $e->die_event;
951
952    return $e->die_event unless 
953       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
954
955         $note->notify_staff($e->requestor->id);
956    $e->create_action_hold_notification($note) or return $e->die_event;
957    $e->commit;
958    return $note->id;
959 }
960
961 __PACKAGE__->register_method (
962         method          => 'create_hold_note',
963         api_name                => 'open-ils.circ.hold_note.create',
964         signature       => q/
965                 Creates a new hold request note object
966                 @param authtoken The login session key
967                 @param note The hold note object to create
968                 @return ID of the new object on success, Event on error
969                 /
970 );
971
972 sub create_hold_note {
973    my( $self, $conn, $auth, $note ) = @_;
974    my $e = new_editor(authtoken=>$auth, xact=>1);
975    return $e->die_event unless $e->checkauth;
976
977    my $hold = $e->retrieve_action_hold_request($note->hold)
978       or return $e->die_event;
979    my $patron = $e->retrieve_actor_user($hold->usr) 
980       or return $e->die_event;
981
982    return $e->die_event unless 
983       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
984
985    $e->create_action_hold_request_note($note) or return $e->die_event;
986    $e->commit;
987    return $note->id;
988 }
989
990 __PACKAGE__->register_method(
991         method  => 'reset_hold',
992         api_name        => 'open-ils.circ.hold.reset',
993         signature       => q/
994                 Un-captures and un-targets a hold, essentially returning
995                 it to the state it was in directly after it was placed,
996                 then attempts to re-target the hold
997                 @param authtoken The login session key
998                 @param holdid The id of the hold
999         /
1000 );
1001
1002
1003 sub reset_hold {
1004         my( $self, $conn, $auth, $holdid ) = @_;
1005         my $reqr;
1006         my ($hold, $evt) = $U->fetch_hold($holdid);
1007         return $evt if $evt;
1008         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1009         return $evt if $evt;
1010         $evt = _reset_hold($self, $reqr, $hold);
1011         return $evt if $evt;
1012         return 1;
1013 }
1014
1015
1016 __PACKAGE__->register_method(
1017         method  => 'reset_hold_batch',
1018         api_name        => 'open-ils.circ.hold.reset.batch'
1019 );
1020
1021 sub reset_hold_batch {
1022     my($self, $conn, $auth, $hold_ids) = @_;
1023
1024     my $e = new_editor(authtoken => $auth);
1025     return $e->event unless $e->checkauth;
1026
1027     for my $hold_id ($hold_ids) {
1028
1029         my $hold = $e->retrieve_action_hold_request(
1030             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}]) 
1031             or return $e->event;
1032
1033             next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1034         _reset_hold($self, $e->requestor, $hold);
1035     }
1036
1037     return 1;
1038 }
1039
1040
1041 sub _reset_hold {
1042         my ($self, $reqr, $hold) = @_;
1043
1044         my $e = new_editor(xact =>1, requestor => $reqr);
1045
1046         $logger->info("reseting hold ".$hold->id);
1047
1048         my $hid = $hold->id;
1049
1050         if( $hold->capture_time and $hold->current_copy ) {
1051
1052                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1053                         or return $e->event;
1054
1055                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1056                         $logger->info("setting copy to status 'reshelving' on hold retarget");
1057                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1058                         $copy->editor($e->requestor->id);
1059                         $copy->edit_date('now');
1060                         $e->update_asset_copy($copy) or return $e->event;
1061
1062                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1063
1064                         # We don't want the copy to remain "in transit"
1065                         $copy->status(OILS_COPY_STATUS_RESHELVING);
1066                         $logger->warn("! reseting hold [$hid] that is in transit");
1067                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1068
1069                         if( $transid ) {
1070                                 my $trans = $e->retrieve_action_transit_copy($transid);
1071                                 if( $trans ) {
1072                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1073                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1074                                         $logger->info("Transit abort completed with result $evt");
1075                                         return $evt unless "$evt" eq 1;
1076                                 }
1077                         }
1078                 }
1079         }
1080
1081         $hold->clear_capture_time;
1082         $hold->clear_current_copy;
1083
1084         $e->update_action_hold_request($hold) or return $e->event;
1085         $e->commit;
1086
1087         $U->storagereq(
1088                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1089
1090         return undef;
1091 }
1092
1093
1094 __PACKAGE__->register_method(
1095         method => 'fetch_open_title_holds',
1096         api_name        => 'open-ils.circ.open_holds.retrieve',
1097         signature       => q/
1098                 Returns a list ids of un-fulfilled holds for a given title id
1099                 @param authtoken The login session key
1100                 @param id the id of the item whose holds we want to retrieve
1101                 @param type The hold type - M, T, V, C
1102         /
1103 );
1104
1105 sub fetch_open_title_holds {
1106         my( $self, $conn, $auth, $id, $type, $org ) = @_;
1107         my $e = new_editor( authtoken => $auth );
1108         return $e->event unless $e->checkauth;
1109
1110         $type ||= "T";
1111         $org ||= $e->requestor->ws_ou;
1112
1113 #       return $e->search_action_hold_request(
1114 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1115
1116         # XXX make me return IDs in the future ^--
1117         my $holds = $e->search_action_hold_request(
1118                 { 
1119                         target                          => $id, 
1120                         cancel_time                     => undef, 
1121                         hold_type                       => $type, 
1122                         fulfillment_time        => undef 
1123                 }
1124         );
1125
1126         flesh_hold_transits($holds);
1127         return $holds;
1128 }
1129
1130
1131 sub flesh_hold_transits {
1132         my $holds = shift;
1133         for my $hold ( @$holds ) {
1134                 $hold->transit(
1135                         $apputils->simplereq(
1136                                 'open-ils.cstore',
1137                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1138                                 { hold => $hold->id },
1139                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
1140                         )->[0]
1141                 );
1142         }
1143 }
1144
1145 sub flesh_hold_notices {
1146         my( $holds, $e ) = @_;
1147         $e ||= new_editor();
1148
1149         for my $hold (@$holds) {
1150                 my $notices = $e->search_action_hold_notification(
1151                         [
1152                                 { hold => $hold->id },
1153                                 { order_by => { anh => 'notify_time desc' } },
1154                         ],
1155                         {idlist=>1}
1156                 );
1157
1158                 $hold->notify_count(scalar(@$notices));
1159                 if( @$notices ) {
1160                         my $n = $e->retrieve_action_hold_notification($$notices[0])
1161                                 or return $e->event;
1162                         $hold->notify_time($n->notify_time);
1163                 }
1164         }
1165 }
1166
1167
1168 __PACKAGE__->register_method(
1169         method => 'fetch_captured_holds',
1170         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1171     stream => 1,
1172         signature       => q/
1173                 Returns a list of un-fulfilled holds for a given title id
1174                 @param authtoken The login session key
1175                 @param org The org id of the location in question
1176         /
1177 );
1178
1179 __PACKAGE__->register_method(
1180         method => 'fetch_captured_holds',
1181         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1182     stream => 1,
1183         signature       => q/
1184                 Returns a list ids of un-fulfilled holds for a given title id
1185                 @param authtoken The login session key
1186                 @param org The org id of the location in question
1187         /
1188 );
1189
1190 sub fetch_captured_holds {
1191         my( $self, $conn, $auth, $org ) = @_;
1192
1193         my $e = new_editor(authtoken => $auth);
1194         return $e->event unless $e->checkauth;
1195         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1196
1197         $org ||= $e->requestor->ws_ou;
1198
1199     my $hold_ids = $e->json_query(
1200         { 
1201             select => { ahr => ['id'] },
1202             from => {
1203                 ahr => {
1204                     acp => {
1205                         field => 'id',
1206                         fkey => 'current_copy'
1207                     },
1208                 }
1209             }, 
1210             where => {
1211                 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1212                 '+ahr' => {
1213                     capture_time                => { "!=" => undef },
1214                     current_copy                => { "!=" => undef },
1215                     fulfillment_time    => undef,
1216                     pickup_lib                  => $org,
1217                     cancel_time                 => undef,
1218                 }
1219             }
1220         },
1221     );
1222
1223     for my $hold_id (@$hold_ids) {
1224         if($self->api_name =~ /id_list/) {
1225             $conn->respond($hold_id->{id});
1226             next;
1227         } else {
1228             $conn->respond(
1229                 $e->retrieve_action_hold_request([
1230                     $hold_id->{id},
1231                     {
1232                         flesh => 1,
1233                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1234                         order_by => {anh => 'notify_time desc'}
1235                     }
1236                 ])
1237             );
1238         }
1239     }
1240
1241     return undef;
1242 }
1243 __PACKAGE__->register_method(
1244         method  => "check_title_hold",
1245         api_name        => "open-ils.circ.title_hold.is_possible",
1246         notes           => q/
1247                 Determines if a hold were to be placed by a given user,
1248                 whether or not said hold would have any potential copies
1249                 to fulfill it.
1250                 @param authtoken The login session key
1251                 @param params A hash of named params including:
1252                         patronid  - the id of the hold recipient
1253                         titleid (brn) - the id of the title to be held
1254                         depth   - the hold range depth (defaults to 0)
1255         /);
1256
1257 sub check_title_hold {
1258         my( $self, $client, $authtoken, $params ) = @_;
1259
1260         my %params              = %$params;
1261         my $titleid             = $params{titleid} ||"";
1262         my $volid               = $params{volume_id};
1263         my $copyid              = $params{copy_id};
1264         my $mrid                = $params{mrid} ||"";
1265         my $depth               = $params{depth} || 0;
1266         my $pickup_lib  = $params{pickup_lib};
1267         my $hold_type   = $params{hold_type} || 'T';
1268     my $selection_ou = $params{selection_ou} || $pickup_lib;
1269
1270         my $e = new_editor(authtoken=>$authtoken);
1271         return $e->event unless $e->checkauth;
1272         my $patron = $e->retrieve_actor_user($params{patronid})
1273                 or return $e->event;
1274
1275         if( $e->requestor->id ne $patron->id ) {
1276                 return $e->event unless 
1277                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1278         }
1279
1280         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1281
1282         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1283                 or return $e->event;
1284
1285     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1286     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1287
1288     if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1289         # work up the tree and as soon as we find a potential copy, use that depth
1290         # also, make sure we don't go past the hard boundary if it exists
1291
1292         # our min boundary is the greater of user-specified boundary or hard boundary
1293         my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?  
1294             $hard_boundary : $$params{depth};
1295
1296         my $depth = $soft_boundary;
1297         while($depth >= $min_depth) {
1298             $logger->info("performing hold possibility check with soft boundary $depth");
1299             my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1300             return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1301             $depth--;
1302         }
1303         return {success => 0};
1304
1305     } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1306         # there is no soft boundary, enforce the hard boundary if it exists
1307         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1308         my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1309         if($status[0]) {
1310             return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1311         } else {
1312             return {success => 0};
1313         }
1314
1315     } else {
1316         # no boundaries defined, fall back to user specifed boundary or no boundary
1317         $logger->info("performing hold possibility check with no boundary");
1318         my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1319         if($status[0]) {
1320             return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1321         } else {
1322             return {success => 0};
1323         }
1324     }
1325 }
1326
1327 sub do_possibility_checks {
1328     my($e, $patron, $request_lib, $depth, %params) = @_;
1329
1330         my $titleid             = $params{titleid} ||"";
1331         my $volid               = $params{volume_id};
1332         my $copyid              = $params{copy_id};
1333         my $mrid                = $params{mrid} ||"";
1334         my $pickup_lib  = $params{pickup_lib};
1335         my $hold_type   = $params{hold_type} || 'T';
1336     my $selection_ou = $params{selection_ou} || $pickup_lib;
1337
1338
1339         my $copy;
1340         my $volume;
1341         my $title;
1342
1343         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1344
1345                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1346                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1347                         or return $e->event;
1348                 $title = $e->retrieve_biblio_record_entry($volume->record)
1349                         or return $e->event;
1350                 return verify_copy_for_hold( 
1351                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1352
1353         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1354
1355                 $volume = $e->retrieve_asset_call_number($volid)
1356                         or return $e->event;
1357                 $title = $e->retrieve_biblio_record_entry($volume->record)
1358                         or return $e->event;
1359
1360                 return _check_volume_hold_is_possible(
1361                         $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1362
1363         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1364
1365                 return _check_title_hold_is_possible(
1366                         $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1367
1368         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1369
1370                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1371                 my @recs = map { $_->source } @$maps;
1372                 for my $rec (@recs) {
1373             my @status = _check_title_hold_is_possible(
1374                                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1375             return @status if $status[1];
1376                 }
1377                 return (0);     
1378         }
1379 }
1380
1381 my %prox_cache;
1382 sub create_ranged_org_filter {
1383     my($e, $selection_ou, $depth) = @_;
1384
1385     # find the orgs from which this hold may be fulfilled, 
1386     # based on the selection_ou and depth
1387
1388     my $top_org = $e->search_actor_org_unit([
1389         {parent_ou => undef}, 
1390         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1391     my %org_filter;
1392
1393     return () if $depth == $top_org->ou_type->depth;
1394
1395     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1396     %org_filter = (circ_lib => []);
1397     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1398
1399     $logger->info("hold org filter at depth $depth and selection_ou ".
1400         "$selection_ou created list of @{$org_filter{circ_lib}}");
1401
1402     return %org_filter;
1403 }
1404
1405
1406 sub _check_title_hold_is_possible {
1407         my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1408    
1409     my $e = new_editor();
1410     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1411
1412     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1413     my $copies = $e->json_query(
1414         { 
1415             select => { acp => ['id', 'circ_lib'] },
1416             from => {
1417                 acp => {
1418                     acn => {
1419                         field => 'id',
1420                         fkey => 'call_number',
1421                         'join' => {
1422                             bre => {
1423                                 field => 'id',
1424                                 filter => { id => $titleid },
1425                                 fkey => 'record'
1426                             }
1427                         }
1428                     },
1429                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1430                     ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1431                 }
1432             }, 
1433             where => {
1434                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1435             }
1436         }
1437     );
1438
1439    $logger->info("title possible found ".scalar(@$copies)." potential copies");
1440    return (0) unless @$copies;
1441
1442    # -----------------------------------------------------------------------
1443    # sort the copies into buckets based on their circ_lib proximity to 
1444    # the patron's home_ou.  
1445    # -----------------------------------------------------------------------
1446
1447    my $home_org = $patron->home_ou;
1448    my $req_org = $request_lib->id;
1449
1450     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1451
1452     $prox_cache{$home_org} = 
1453         $e->search_actor_org_unit_proximity({from_org => $home_org})
1454         unless $prox_cache{$home_org};
1455     my $home_prox = $prox_cache{$home_org};
1456
1457    my %buckets;
1458    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1459    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1460
1461    my @keys = sort { $a <=> $b } keys %buckets;
1462
1463
1464    if( $home_org ne $req_org ) {
1465       # -----------------------------------------------------------------------
1466       # shove the copies close to the request_lib into the primary buckets 
1467       # directly before the farthest away copies.  That way, they are not 
1468       # given priority, but they are checked before the farthest copies.
1469       # -----------------------------------------------------------------------
1470         $prox_cache{$req_org} = 
1471             $e->search_actor_org_unit_proximity({from_org => $req_org})
1472             unless $prox_cache{$req_org};
1473         my $req_prox = $prox_cache{$req_org};
1474
1475
1476       my %buckets2;
1477       my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1478       push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1479
1480       my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
1481       my $new_key = $highest_key - 0.5; # right before the farthest prox
1482       my @keys2 = sort { $a <=> $b } keys %buckets2;
1483       for my $key (@keys2) {
1484          last if $key >= $highest_key;
1485          push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1486       }
1487    }
1488
1489    @keys = sort { $a <=> $b } keys %buckets;
1490
1491    my $title;
1492    my %seen;
1493    for my $key (@keys) {
1494       my @cps = @{$buckets{$key}};
1495
1496       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1497
1498       for my $copyid (@cps) {
1499
1500          next if $seen{$copyid};
1501          $seen{$copyid} = 1; # there could be dupes given the merged buckets
1502          my $copy = $e->retrieve_asset_copy($copyid);
1503          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1504
1505          unless($title) { # grab the title if we don't already have it
1506             my $vol = $e->retrieve_asset_call_number(
1507                [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1508             $title = $vol->record;
1509          }
1510    
1511          my @status = verify_copy_for_hold( 
1512             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1513
1514         return @status if $status[0];
1515       }
1516    }
1517
1518    return (0);
1519 }
1520
1521
1522 sub _check_volume_hold_is_possible {
1523         my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1524     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1525         my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1526         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1527         for my $copy ( @$copies ) {
1528         my @status = verify_copy_for_hold( 
1529                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1530         return @status if $status[0];
1531         }
1532         return (0);
1533 }
1534
1535
1536
1537 sub verify_copy_for_hold {
1538         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1539         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1540     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1541                 {       patron                          => $patron, 
1542                         requestor                       => $requestor, 
1543                         copy                            => $copy,
1544                         title                           => $title, 
1545                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1546                         pickup_lib                      => $pickup_lib,
1547                         request_lib                     => $request_lib,
1548             new_hold            => 1
1549                 } 
1550         );
1551
1552     return (
1553         $permitted,
1554         (
1555                 ($copy->circ_lib == $pickup_lib) and 
1556             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1557         )
1558     );
1559 }
1560
1561
1562
1563 sub find_nearest_permitted_hold {
1564
1565         my $class       = shift;
1566         my $editor      = shift; # CStoreEditor object
1567         my $copy                = shift; # copy to target
1568         my $user                = shift; # staff 
1569         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1570         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1571
1572         my $bc = $copy->barcode;
1573
1574         # find any existing holds that already target this copy
1575         my $old_holds = $editor->search_action_hold_request(
1576                 {       current_copy => $copy->id, 
1577                         cancel_time => undef, 
1578                         capture_time => undef 
1579                 } 
1580         );
1581
1582         # hold->type "R" means we need this copy
1583         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1584
1585
1586     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1587
1588         $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1589         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1590
1591         # search for what should be the best holds for this copy to fulfill
1592         my $best_holds = $U->storagereq(
1593                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1594                 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1595
1596         unless(@$best_holds) {
1597
1598                 if( my $hold = $$old_holds[0] ) {
1599                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1600                         return ($hold);
1601                 }
1602
1603                 $logger->info("circulator: no suitable holds found for copy $bc");
1604                 return (undef, $evt);
1605         }
1606
1607
1608         my $best_hold;
1609
1610         # for each potential hold, we have to run the permit script
1611         # to make sure the hold is actually permitted.
1612         for my $holdid (@$best_holds) {
1613                 next unless $holdid;
1614                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1615
1616                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1617                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1618                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1619
1620                 # see if this hold is permitted
1621                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1622                         {       patron_id                       => $hold->usr,
1623                                 requestor                       => $reqr,
1624                                 copy                            => $copy,
1625                                 pickup_lib                      => $hold->pickup_lib,
1626                                 request_lib                     => $rlib,
1627                         } 
1628                 );
1629
1630                 if( $permitted ) {
1631                         $best_hold = $hold;
1632                         last;
1633                 }
1634         }
1635
1636
1637         unless( $best_hold ) { # no "good" permitted holds were found
1638                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1639                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1640                         return ($hold);
1641                 }
1642
1643                 # we got nuthin
1644                 $logger->info("circulator: no suitable holds found for copy $bc");
1645                 return (undef, $evt);
1646         }
1647
1648         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1649
1650         # indicate a permitted hold was found
1651         return $best_hold if $check_only;
1652
1653         # we've found a permitted hold.  we need to "grab" the copy 
1654         # to prevent re-targeted holds (next part) from re-grabbing the copy
1655         $best_hold->current_copy($copy->id);
1656         $editor->update_action_hold_request($best_hold) 
1657                 or return (undef, $editor->event);
1658
1659
1660     my @retarget;
1661
1662         # re-target any other holds that already target this copy
1663         for my $old_hold (@$old_holds) {
1664                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1665                 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1666             $old_hold->id." after a better hold [".$best_hold->id."] was found");
1667         $old_hold->clear_current_copy;
1668         $old_hold->clear_prev_check_time;
1669         $editor->update_action_hold_request($old_hold) 
1670             or return (undef, $editor->event);
1671         push(@retarget, $old_hold->id);
1672         }
1673
1674         return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1675 }
1676
1677
1678
1679
1680
1681
1682 __PACKAGE__->register_method(
1683         method => 'all_rec_holds',
1684         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1685 );
1686
1687 sub all_rec_holds {
1688         my( $self, $conn, $auth, $title_id, $args ) = @_;
1689
1690         my $e = new_editor(authtoken=>$auth);
1691         $e->checkauth or return $e->event;
1692         $e->allowed('VIEW_HOLD') or return $e->event;
1693
1694         $args ||= {};
1695     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
1696         $args->{cancel_time} = undef;
1697
1698         my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1699
1700     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1701     if($mr_map) {
1702         $resp->{metarecord_holds} = $e->search_action_hold_request(
1703             {   hold_type => OILS_HOLD_TYPE_METARECORD,
1704                 target => $mr_map->metarecord,
1705                 %$args 
1706             }, {idlist => 1}
1707         );
1708     }
1709
1710         $resp->{title_holds} = $e->search_action_hold_request(
1711                 { 
1712                         hold_type => OILS_HOLD_TYPE_TITLE, 
1713                         target => $title_id, 
1714                         %$args 
1715                 }, {idlist=>1} );
1716
1717         my $vols = $e->search_asset_call_number(
1718                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1719
1720         return $resp unless @$vols;
1721
1722         $resp->{volume_holds} = $e->search_action_hold_request(
1723                 { 
1724                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1725                         target => $vols,
1726                         %$args }, 
1727                 {idlist=>1} );
1728
1729         my $copies = $e->search_asset_copy(
1730                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1731
1732         return $resp unless @$copies;
1733
1734         $resp->{copy_holds} = $e->search_action_hold_request(
1735                 { 
1736                         hold_type => OILS_HOLD_TYPE_COPY,
1737                         target => $copies,
1738                         %$args }, 
1739                 {idlist=>1} );
1740
1741         return $resp;
1742 }
1743
1744
1745
1746
1747
1748 __PACKAGE__->register_method(
1749         method => 'uber_hold',
1750     authoritative => 1,
1751         api_name => 'open-ils.circ.hold.details.retrieve'
1752 );
1753
1754 sub uber_hold {
1755         my($self, $client, $auth, $hold_id) = @_;
1756         my $e = new_editor(authtoken=>$auth);
1757         $e->checkauth or return $e->event;
1758         $e->allowed('VIEW_HOLD') or return $e->event;
1759
1760         my $resp = {};
1761
1762         my $hold = $e->retrieve_action_hold_request(
1763                 [
1764                         $hold_id,
1765                         {
1766                                 flesh => 1,
1767                                 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1768                         }
1769                 ]
1770         ) or return $e->event;
1771
1772         my $user = $hold->usr;
1773         $hold->usr($user->id);
1774
1775         my $card = $e->retrieve_actor_card($user->card)
1776                 or return $e->event;
1777
1778         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1779
1780         flesh_hold_notices([$hold], $e);
1781         flesh_hold_transits([$hold]);
1782
1783     my $details = retrieve_hold_queue_status_impl($e, $hold);
1784
1785         return {
1786                 hold            => $hold,
1787                 copy            => $copy,
1788                 volume  => $volume,
1789                 mvr             => $mvr,
1790                 patron_first => $user->first_given_name,
1791                 patron_last  => $user->family_name,
1792                 patron_barcode => $card->barcode,
1793         %$details
1794         };
1795 }
1796
1797
1798
1799 # -----------------------------------------------------
1800 # Returns the MVR object that represents what the
1801 # hold is all about
1802 # -----------------------------------------------------
1803 sub find_hold_mvr {
1804         my( $e, $hold ) = @_;
1805
1806         my $tid;
1807         my $copy;
1808         my $volume;
1809
1810         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1811                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1812                         or return $e->event;
1813                 $tid = $mr->master_record;
1814
1815         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1816                 $tid = $hold->target;
1817
1818         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1819                 $volume = $e->retrieve_asset_call_number($hold->target)
1820                         or return $e->event;
1821                 $tid = $volume->record;
1822
1823         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1824                 $copy = $e->retrieve_asset_copy($hold->target)
1825                         or return $e->event;
1826                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1827                         or return $e->event;
1828                 $tid = $volume->record;
1829         }
1830
1831         if(!$copy and ref $hold->current_copy ) {
1832                 $copy = $hold->current_copy;
1833                 $hold->current_copy($copy->id);
1834         }
1835
1836         if(!$volume and $copy) {
1837                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1838         }
1839
1840         my $title = $e->retrieve_biblio_record_entry($tid);
1841         return ( $U->record_to_mvr($title), $volume, $copy );
1842 }
1843
1844
1845
1846
1847 1;