]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
updated manual hold notification to use the editor+transaction
[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 =head old
674 sub __create_hold_notify {
675         my( $self, $conn, $authtoken, $notification ) = @_;
676         my( $requestor, $evt ) = $U->checkses($authtoken);
677         return $evt if $evt;
678         my ($hold, $patron);
679         ($hold, $evt) = $U->fetch_hold($notification->hold);
680         return $evt if $evt;
681         ($patron, $evt) = $U->fetch_user($hold->usr);
682         return $evt if $evt;
683
684         # XXX perm depth probably doesn't matter here -- should always be consortium level
685         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
686         return $evt if $evt;
687
688         # Set the proper notifier 
689         $notification->notify_staff($requestor->id);
690         my $id = $U->storagereq(
691                 'open-ils.storage.direct.action.hold_notification.create', $notification );
692         return $U->DB_UPDATE_FAILED($notification) unless $id;
693         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
694         return $id;
695 }
696 =cut
697
698 sub create_hold_notify {
699    my( $self, $conn, $auth, $note ) = @_;
700    my $e = new_editor(authtoken=>$auth, xact=>1);
701    return $e->die_event unless $e->checkauth;
702
703    my $hold = $e->retrieve_action_hold_request($note->hold)
704       or return $e->die_event;
705    my $patron = $e->retrieve_actor_user($hold->usr) 
706       or return $e->die_event;
707
708    return $e->die_event unless 
709       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
710
711         $note->notify_staff($e->requestor->id);
712    $e->create_action_hold_notification($note) or return $e->die_event;
713    $e->commit;
714    return $note->id;
715 }
716
717
718 __PACKAGE__->register_method(
719         method  => 'reset_hold',
720         api_name        => 'open-ils.circ.hold.reset',
721         signature       => q/
722                 Un-captures and un-targets a hold, essentially returning
723                 it to the state it was in directly after it was placed,
724                 then attempts to re-target the hold
725                 @param authtoken The login session key
726                 @param holdid The id of the hold
727         /
728 );
729
730
731 sub reset_hold {
732         my( $self, $conn, $auth, $holdid ) = @_;
733         my $reqr;
734         my ($hold, $evt) = $U->fetch_hold($holdid);
735         return $evt if $evt;
736         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
737         return $evt if $evt;
738         $evt = $self->_reset_hold($reqr, $hold);
739         return $evt if $evt;
740         return 1;
741 }
742
743 sub _reset_hold {
744         my ($self, $reqr, $hold) = @_;
745
746         my $e = new_editor(xact =>1, requestor => $reqr);
747
748         $logger->info("reseting hold ".$hold->id);
749
750         my $hid = $hold->id;
751
752         if( $hold->capture_time and $hold->current_copy ) {
753
754                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
755                         or return $e->event;
756
757                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
758                         $logger->info("setting copy to status 'reshelving' on hold retarget");
759                         $copy->status(OILS_COPY_STATUS_RESHELVING);
760                         $copy->editor($e->requestor->id);
761                         $copy->edit_date('now');
762                         $e->update_asset_copy($copy) or return $e->event;
763
764                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
765
766                         # We don't want the copy to remain "in transit"
767                         $copy->status(OILS_COPY_STATUS_RESHELVING);
768                         $logger->warn("! reseting hold [$hid] that is in transit");
769                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
770
771                         if( $transid ) {
772                                 my $trans = $e->retrieve_action_transit_copy($transid);
773                                 if( $trans ) {
774                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
775                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
776                                         $logger->info("Transit abort completed with result $evt");
777                                         return $evt unless "$evt" eq 1;
778                                 }
779                         }
780                 }
781         }
782
783         $hold->clear_capture_time;
784         $hold->clear_current_copy;
785
786         $e->update_action_hold_request($hold) or return $e->event;
787         $e->commit;
788
789         $U->storagereq(
790                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
791
792         return undef;
793 }
794
795
796 __PACKAGE__->register_method(
797         method => 'fetch_open_title_holds',
798         api_name        => 'open-ils.circ.open_holds.retrieve',
799         signature       => q/
800                 Returns a list ids of un-fulfilled holds for a given title id
801                 @param authtoken The login session key
802                 @param id the id of the item whose holds we want to retrieve
803                 @param type The hold type - M, T, V, C
804         /
805 );
806
807 sub fetch_open_title_holds {
808         my( $self, $conn, $auth, $id, $type, $org ) = @_;
809         my $e = new_editor( authtoken => $auth );
810         return $e->event unless $e->checkauth;
811
812         $type ||= "T";
813         $org ||= $e->requestor->ws_ou;
814
815 #       return $e->search_action_hold_request(
816 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
817
818         # XXX make me return IDs in the future ^--
819         my $holds = $e->search_action_hold_request(
820                 { 
821                         target                          => $id, 
822                         cancel_time                     => undef, 
823                         hold_type                       => $type, 
824                         fulfillment_time        => undef 
825                 }
826         );
827
828         flesh_hold_transits($holds);
829         return $holds;
830 }
831
832
833 sub flesh_hold_transits {
834         my $holds = shift;
835         for my $hold ( @$holds ) {
836                 $hold->transit(
837                         $apputils->simplereq(
838                                 'open-ils.cstore',
839                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
840                                 { hold => $hold->id },
841                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
842                         )->[0]
843                 );
844         }
845 }
846
847 sub flesh_hold_notices {
848         my( $holds, $e ) = @_;
849         $e ||= new_editor();
850
851         for my $hold (@$holds) {
852                 my $notices = $e->search_action_hold_notification(
853                         [
854                                 { hold => $hold->id },
855                                 { order_by => { anh => 'notify_time desc' } },
856                         ],
857                         {idlist=>1}
858                 );
859
860                 $hold->notify_count(scalar(@$notices));
861                 if( @$notices ) {
862                         my $n = $e->retrieve_action_hold_notification($$notices[0])
863                                 or return $e->event;
864                         $hold->notify_time($n->notify_time);
865                 }
866         }
867 }
868
869
870
871
872 __PACKAGE__->register_method(
873         method => 'fetch_captured_holds',
874         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
875         signature       => q/
876                 Returns a list of un-fulfilled holds for a given title id
877                 @param authtoken The login session key
878                 @param org The org id of the location in question
879         /
880 );
881
882 __PACKAGE__->register_method(
883         method => 'fetch_captured_holds',
884         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
885         signature       => q/
886                 Returns a list ids of un-fulfilled holds for a given title id
887                 @param authtoken The login session key
888                 @param org The org id of the location in question
889         /
890 );
891
892 sub fetch_captured_holds {
893         my( $self, $conn, $auth, $org ) = @_;
894
895         my $e = new_editor(authtoken => $auth);
896         return $e->event unless $e->checkauth;
897         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
898
899         $org ||= $e->requestor->ws_ou;
900
901         my $holds = $e->search_action_hold_request(
902                 { 
903                         capture_time            => { "!=" => undef },
904                         current_copy            => { "!=" => undef },
905                         fulfillment_time        => undef,
906                         pickup_lib                      => $org,
907                         cancel_time                     => undef,
908                 }
909         );
910
911         my @res;
912         for my $h (@$holds) {
913                 my $copy = $e->retrieve_asset_copy($h->current_copy)
914                         or return $e->event;
915                 push( @res, $h ) if 
916                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
917         }
918
919         if( ! $self->api_name =~ /id_list/ ) {
920                 flesh_hold_transits(\@res);
921                 flesh_hold_notices(\@res, $e);
922         }
923
924         if( $self->api_name =~ /id_list/ ) {
925                 return [ map { $_->id } @res ];
926         } else {
927                 return \@res;
928         }
929 }
930
931
932 __PACKAGE__->register_method(
933         method  => "check_title_hold",
934         api_name        => "open-ils.circ.title_hold.is_possible",
935         notes           => q/
936                 Determines if a hold were to be placed by a given user,
937                 whether or not said hold would have any potential copies
938                 to fulfill it.
939                 @param authtoken The login session key
940                 @param params A hash of named params including:
941                         patronid  - the id of the hold recipient
942                         titleid (brn) - the id of the title to be held
943                         depth   - the hold range depth (defaults to 0)
944         /);
945
946 sub check_title_hold {
947         my( $self, $client, $authtoken, $params ) = @_;
948
949         my %params              = %$params;
950         my $titleid             = $params{titleid} ||"";
951         my $volid               = $params{volume_id};
952         my $copyid              = $params{copy_id};
953         my $mrid                        = $params{mrid} ||"";
954         my $depth               = $params{depth} || 0;
955         my $pickup_lib  = $params{pickup_lib};
956         my $hold_type   = $params{hold_type} || 'T';
957
958         my $e = new_editor(authtoken=>$authtoken);
959         return $e->event unless $e->checkauth;
960         my $patron = $e->retrieve_actor_user($params{patronid})
961                 or return $e->event;
962
963         if( $e->requestor->id ne $patron->id ) {
964                 return $e->event unless 
965                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
966         }
967
968         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
969
970         my $rangelib    = $params{range_lib} || $patron->home_ou;
971
972         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
973                 or return $e->event;
974
975         $logger->info("checking hold possibility with type $hold_type");
976
977         my $copy;
978         my $volume;
979         my $title;
980
981         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
982
983                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
984                 $volume = $e->retrieve_asset_call_number($copy->call_number)
985                         or return $e->event;
986                 $title = $e->retrieve_biblio_record_entry($volume->record)
987                         or return $e->event;
988                 return verify_copy_for_hold( 
989                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
990
991         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
992
993                 $volume = $e->retrieve_asset_call_number($volid)
994                         or return $e->event;
995                 $title = $e->retrieve_biblio_record_entry($volume->record)
996                         or return $e->event;
997
998                 return _check_volume_hold_is_possible(
999                         $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1000
1001         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1002
1003                 return _check_title_hold_is_possible(
1004                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1005
1006         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1007
1008                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1009                 my @recs = map { $_->source } @$maps;
1010                 for my $rec (@recs) {
1011                         return 1 if (_check_title_hold_is_possible(
1012                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1013                 }
1014                 return 0;       
1015         }
1016 }
1017
1018
1019
1020 sub _check_title_hold_is_possible {
1021         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1022
1023         my $limit       = 10;
1024         my $offset      = 0;
1025         my $title;
1026
1027         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1028
1029         while( $title = $U->storagereq(
1030                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
1031                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1032
1033                 last unless 
1034                         ref($title) and 
1035                         ref($title->call_numbers) and 
1036                         @{$title->call_numbers};
1037
1038                 for my $cn (@{$title->call_numbers}) {
1039         
1040                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1041         
1042                         for my $copy (@{$cn->copies}) {
1043                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1044                                 return 1 if verify_copy_for_hold( 
1045                                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1046                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1047                         }
1048                 }
1049
1050                 $offset += $limit;
1051         }
1052         return 0;
1053 }
1054
1055 sub _check_volume_hold_is_possible {
1056         my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1057         my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1058         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1059         for my $copy ( @$copies ) {
1060                 return 1 if verify_copy_for_hold( 
1061                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1062         }
1063         return 0;
1064 }
1065
1066
1067
1068 sub verify_copy_for_hold {
1069         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1070         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1071         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1072                 {       patron                          => $patron, 
1073                         requestor                       => $requestor, 
1074                         copy                                    => $copy,
1075                         title                                   => $title, 
1076                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1077                         pickup_lib                      => $pickup_lib,
1078                         request_lib                     => $request_lib 
1079                 } 
1080         );
1081         return 0;
1082 }
1083
1084
1085
1086 sub find_nearest_permitted_hold {
1087
1088         my $class       = shift;
1089         my $editor      = shift; # CStoreEditor object
1090         my $copy                = shift; # copy to target
1091         my $user                = shift; # hold recipient
1092         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1093         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1094
1095         my $bc = $copy->barcode;
1096
1097         # find any existing holds that already target this copy
1098         my $old_holds = $editor->search_action_hold_request(
1099                 {       current_copy => $copy->id, 
1100                         cancel_time => undef, 
1101                         capture_time => undef 
1102                 } 
1103         );
1104
1105         # hold->type "R" means we need this copy
1106         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1107
1108         $logger->info("circulator: searching for best hold at org ".$user->ws_ou." and copy $bc");
1109
1110         # search for what should be the best holds for this copy to fulfill
1111         my $best_holds = $U->storagereq(
1112                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1113                 $user->ws_ou, $copy->id, 10 );
1114
1115         unless(@$best_holds) {
1116
1117                 if( my $hold = $$old_holds[0] ) {
1118                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1119                         return ($hold);
1120                 }
1121
1122                 $logger->info("circulator: no suitable holds found for copy $bc");
1123                 return (undef, $evt);
1124         }
1125
1126
1127         my $best_hold;
1128
1129         # for each potential hold, we have to run the permit script
1130         # to make sure the hold is actually permitted.
1131         for my $holdid (@$best_holds) {
1132                 next unless $holdid;
1133                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1134
1135                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1136                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1137                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1138
1139                 # see if this hold is permitted
1140                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1141                         {       patron_id                       => $hold->usr,
1142                                 requestor                       => $reqr->id,
1143                                 copy                                    => $copy,
1144                                 pickup_lib                      => $hold->pickup_lib,
1145                                 request_lib                     => $rlib,
1146                         } 
1147                 );
1148
1149                 if( $permitted ) {
1150                         $best_hold = $hold;
1151                         last;
1152                 }
1153         }
1154
1155
1156         unless( $best_hold ) { # no "good" permitted holds were found
1157                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1158                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1159                         return ($hold);
1160                 }
1161
1162                 # we got nuthin
1163                 $logger->info("circulator: no suitable holds found for copy $bc");
1164                 return (undef, $evt);
1165         }
1166
1167         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1168
1169         # indicate a permitted hold was found
1170         return $best_hold if $check_only;
1171
1172         # we've found a permitted hold.  we need to "grab" the copy 
1173         # to prevent re-targeted holds (next part) from re-grabbing the copy
1174         $best_hold->current_copy($copy->id);
1175         $editor->update_action_hold_request($best_hold) 
1176                 or return (undef, $editor->event);
1177
1178
1179         # re-target any other holds that already target this copy
1180         for my $old_hold (@$old_holds) {
1181                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1182                 $logger->info("circulator: re-targeting hold ".$old_hold->id.
1183                         " after a better hold [".$best_hold->id."] was found");
1184                 $U->storagereq( 
1185                         'open-ils.storage.action.hold_request.copy_targeter', undef, $old_hold->id );
1186         }
1187
1188         return ($best_hold);
1189 }
1190
1191
1192
1193
1194
1195
1196 __PACKAGE__->register_method(
1197         method => 'all_rec_holds',
1198         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1199 );
1200
1201 sub all_rec_holds {
1202         my( $self, $conn, $auth, $title_id, $args ) = @_;
1203
1204         my $e = new_editor(authtoken=>$auth);
1205         $e->checkauth or return $e->event;
1206         $e->allowed('VIEW_HOLD') or return $e->event;
1207
1208         $args ||= { fulfillment_time => undef };
1209         $args->{cancel_time} = undef;
1210
1211         my $resp = { volume_holds => [], copy_holds => [] };
1212
1213         $resp->{title_holds} = $e->search_action_hold_request(
1214                 { 
1215                         hold_type => OILS_HOLD_TYPE_TITLE, 
1216                         target => $title_id, 
1217                         %$args 
1218                 }, {idlist=>1} );
1219
1220         my $vols = $e->search_asset_call_number(
1221                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1222
1223         return $resp unless @$vols;
1224
1225         $resp->{volume_holds} = $e->search_action_hold_request(
1226                 { 
1227                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1228                         target => $vols,
1229                         %$args }, 
1230                 {idlist=>1} );
1231
1232         my $copies = $e->search_asset_copy(
1233                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1234
1235         return $resp unless @$copies;
1236
1237         $resp->{copy_holds} = $e->search_action_hold_request(
1238                 { 
1239                         hold_type => OILS_HOLD_TYPE_COPY,
1240                         target => $copies,
1241                         %$args }, 
1242                 {idlist=>1} );
1243
1244         return $resp;
1245 }
1246
1247
1248
1249
1250
1251 __PACKAGE__->register_method(
1252         method => 'uber_hold',
1253         api_name => 'open-ils.circ.hold.details.retrieve'
1254 );
1255
1256 sub uber_hold {
1257         my($self, $client, $auth, $hold_id) = @_;
1258         my $e = new_editor(authtoken=>$auth);
1259         $e->checkauth or return $e->event;
1260         $e->allowed('VIEW_HOLD') or return $e->event;
1261
1262         my $resp = {};
1263
1264         my $hold = $e->retrieve_action_hold_request(
1265                 [
1266                         $hold_id,
1267                         {
1268                                 flesh => 1,
1269                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1270                         }
1271                 ]
1272         ) or return $e->event;
1273
1274         my $user = $hold->usr;
1275         $hold->usr($user->id);
1276
1277         my $card = $e->retrieve_actor_card($user->card)
1278                 or return $e->event;
1279
1280         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1281
1282         flesh_hold_notices([$hold], $e);
1283         flesh_hold_transits([$hold]);
1284
1285         return {
1286                 hold            => $hold,
1287                 copy            => $copy,
1288                 volume  => $volume,
1289                 mvr             => $mvr,
1290                 status  => _hold_status($e, $hold),
1291                 patron_first => $user->first_given_name,
1292                 patron_last  => $user->family_name,
1293                 patron_barcode => $card->barcode,
1294         };
1295 }
1296
1297
1298
1299 # -----------------------------------------------------
1300 # Returns the MVR object that represents what the
1301 # hold is all about
1302 # -----------------------------------------------------
1303 sub find_hold_mvr {
1304         my( $e, $hold ) = @_;
1305
1306         my $tid;
1307         my $copy;
1308         my $volume;
1309
1310         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1311                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1312                         or return $e->event;
1313                 $tid = $mr->master_record;
1314
1315         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1316                 $tid = $hold->target;
1317
1318         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1319                 $volume = $e->retrieve_asset_call_number($hold->target)
1320                         or return $e->event;
1321                 $tid = $volume->record;
1322
1323         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1324                 $copy = $e->retrieve_asset_copy($hold->target)
1325                         or return $e->event;
1326                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1327                         or return $e->event;
1328                 $tid = $volume->record;
1329         }
1330
1331         if(!$copy and ref $hold->current_copy ) {
1332                 $copy = $hold->current_copy;
1333                 $hold->current_copy($copy->id);
1334         }
1335
1336         if(!$volume and $copy) {
1337                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1338         }
1339
1340         my $title = $e->retrieve_biblio_record_entry($tid);
1341         return ( $U->record_to_mvr($title), $volume, $copy );
1342 }
1343
1344
1345
1346
1347 1;