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