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