(bug #3584) detect ccl queries
[koha.git] / C4 / Reserves.pm
1 package C4::Reserves;
2
3 # Copyright 2000-2002 Katipo Communications
4 #           2006 SAN Ouest Provence
5 #           2007 BibLibre Paul POULAIN
6 #
7 # This file is part of Koha.
8 #
9 # Koha is free software; you can redistribute it and/or modify it under the
10 # terms of the GNU General Public License as published by the Free Software
11 # Foundation; either version 2 of the License, or (at your option) any later
12 # version.
13 #
14 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License along with
19 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
20 # Suite 330, Boston, MA  02111-1307 USA
21
22
23 use strict;
24 # use warnings;  # FIXME: someday
25 use C4::Context;
26 use C4::Biblio;
27 use C4::Members;
28 use C4::Items;
29 use C4::Search;
30 use C4::Circulation;
31 use C4::Accounts;
32
33 # for _koha_notify_reserve
34 use C4::Members::Messaging;
35 use C4::Members qw( GetMember );
36 use C4::Letters;
37 use C4::Branch qw( GetBranchDetail );
38 use List::MoreUtils qw( firstidx );
39
40 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
41
42 =head1 NAME
43
44 C4::Reserves - Koha functions for dealing with reservation.
45
46 =head1 SYNOPSIS
47
48   use C4::Reserves;
49
50 =head1 DESCRIPTION
51
52   this modules provides somes functions to deal with reservations.
53   
54   Reserves are stored in reserves table.
55   The following columns contains important values :
56   - priority >0      : then the reserve is at 1st stage, and not yet affected to any item.
57              =0      : then the reserve is being dealed
58   - found : NULL       : means the patron requested the 1st available, and we haven't choosen the item
59             W(aiting)  : the reserve has an itemnumber affected, and is on the way
60             F(inished) : the reserve has been completed, and is done
61   - itemnumber : empty : the reserve is still unaffected to an item
62                  filled: the reserve is attached to an item
63   The complete workflow is :
64   ==== 1st use case ====
65   patron request a document, 1st available :                      P >0, F=NULL, I=NULL
66   a library having it run "transfertodo", and clic on the list    
67          if there is no transfer to do, the reserve waiting
68          patron can pick it up                                    P =0, F=W,    I=filled 
69          if there is a transfer to do, write in branchtransfer    P =0, F=NULL, I=filled
70            The pickup library recieve the book, it check in       P =0, F=W,    I=filled
71   The patron borrow the book                                      P =0, F=F,    I=filled
72   
73   ==== 2nd use case ====
74   patron requests a document, a given item,
75     If pickup is holding branch                                   P =0, F=W,   I=filled
76     If transfer needed, write in branchtransfer                   P =0, F=NULL, I=filled
77         The pickup library recieve the book, it checks it in      P =0, F=W,    I=filled
78   The patron borrow the book                                      P =0, F=F,    I=filled
79   
80 =head1 FUNCTIONS
81
82 =over 2
83
84 =cut
85
86 BEGIN {
87     # set the version for version checking
88     $VERSION = 3.01;
89         require Exporter;
90     @ISA = qw(Exporter);
91     @EXPORT = qw(
92         &AddReserve
93   
94         &GetReservesFromItemnumber
95         &GetReservesFromBiblionumber
96         &GetReservesFromBorrowernumber
97         &GetReservesForBranch
98         &GetReservesToBranch
99         &GetReserveCount
100         &GetReserveFee
101                 &GetReserveInfo
102     
103         &GetOtherReserves
104         
105         &ModReserveFill
106         &ModReserveAffect
107         &ModReserve
108         &ModReserveStatus
109         &ModReserveCancelAll
110         &ModReserveMinusPriority
111         
112         &CheckReserves
113         &CancelReserve
114
115         &IsAvailableForItemLevelRequest
116     );
117 }    
118
119 =item AddReserve
120
121     AddReserve($branch,$borrowernumber,$biblionumber,$constraint,$bibitems,$priority,$notes,$title,$checkitem,$found)
122
123 =cut
124
125 sub AddReserve {
126     my (
127         $branch,    $borrowernumber, $biblionumber,
128         $constraint, $bibitems,  $priority,       $notes,
129         $title,      $checkitem, $found
130     ) = @_;
131     my $fee =
132           GetReserveFee($borrowernumber, $biblionumber, $constraint,
133             $bibitems );
134     my $dbh     = C4::Context->dbh;
135     my $const   = lc substr( $constraint, 0, 1 );
136     my @datearr = localtime(time);
137     my $resdate =
138       ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
139     my $waitingdate;
140
141     # If the reserv had the waiting status, we had the value of the resdate
142     if ( $found eq 'W' ) {
143         $waitingdate = $resdate;
144     }
145
146     #eval {
147     # updates take place here
148     if ( $fee > 0 ) {
149         my $nextacctno = &getnextacctno( $borrowernumber );
150         my $query      = qq/
151         INSERT INTO accountlines
152             (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
153         VALUES
154             (?,?,now(),?,?,'Res',?)
155     /;
156         my $usth = $dbh->prepare($query);
157         $usth->execute( $borrowernumber, $nextacctno, $fee,
158             "Reserve Charge - $title", $fee );
159     }
160
161     #if ($const eq 'a'){
162     my $query = qq/
163         INSERT INTO reserves
164             (borrowernumber,biblionumber,reservedate,branchcode,constrainttype,
165             priority,reservenotes,itemnumber,found,waitingdate)
166         VALUES
167              (?,?,?,?,?,
168              ?,?,?,?,?)
169     /;
170     my $sth = $dbh->prepare($query);
171     $sth->execute(
172         $borrowernumber, $biblionumber, $resdate, $branch,
173         $const,          $priority,     $notes,   $checkitem,
174         $found,          $waitingdate
175     );
176
177     # Send e-mail to librarian if syspref is active
178     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
179         my $borrower = GetMemberDetails($borrowernumber);
180         my $biblio   = GetBiblioData($biblionumber);
181         my $letter = C4::Letters::getletter( 'reserves', 'HOLDPLACED');
182         my $admin_email_address = C4::Context->preference('KohaAdminEmailAddress');
183
184         my %keys = (%$borrower, %$biblio);
185         foreach my $key (keys %keys) {
186             my $replacefield = "<<$key>>";
187             $letter->{content} =~ s/$replacefield/$keys{$key}/g;
188             $letter->{title} =~ s/$replacefield/$keys{$key}/g;
189         }
190         
191         C4::Letters::EnqueueLetter(
192                             {   letter                 => $letter,
193                                 borrowernumber         => $borrowernumber,
194                                 message_transport_type => 'email',
195                                 from_address           => $admin_email_address,
196                                 to_address           => $admin_email_address,
197                             }
198                         );
199         
200
201     }
202
203
204     #}
205     ($const eq "o" || $const eq "e") or return;   # FIXME: why not have a useful return value?
206     $query = qq/
207         INSERT INTO reserveconstraints
208             (borrowernumber,biblionumber,reservedate,biblioitemnumber)
209         VALUES
210             (?,?,?,?)
211     /;
212     $sth = $dbh->prepare($query);    # keep prepare outside the loop!
213     foreach (@$bibitems) {
214         $sth->execute($borrowernumber, $biblionumber, $resdate, $_);
215     }
216         
217     return;     # FIXME: why not have a useful return value?
218 }
219
220 =item GetReservesFromBiblionumber
221
222 ($count, $title_reserves) = &GetReserves($biblionumber);
223
224 This function gets the list of reservations for one C<$biblionumber>, returning a count
225 of the reserves and an arrayref pointing to the reserves for C<$biblionumber>.
226
227 =cut
228
229 sub GetReservesFromBiblionumber {
230     my ($biblionumber) = shift or return (0, []);
231     my $dbh   = C4::Context->dbh;
232
233     # Find the desired items in the reserves
234     my $query = "
235         SELECT  branchcode,
236                 timestamp AS rtimestamp,
237                 priority,
238                 biblionumber,
239                 borrowernumber,
240                 reservedate,
241                 constrainttype,
242                 found,
243                 itemnumber,
244                 reservenotes
245         FROM     reserves
246         WHERE biblionumber = ?
247         ORDER BY priority";
248     my $sth = $dbh->prepare($query);
249     $sth->execute($biblionumber);
250     my @results;
251     my $i = 0;
252     while ( my $data = $sth->fetchrow_hashref ) {
253
254         # FIXME - What is this doing? How do constraints work?
255         if ($data->{constrainttype} eq 'o') {
256             $query = '
257                 SELECT biblioitemnumber
258                 FROM  reserveconstraints
259                 WHERE  biblionumber   = ?
260                 AND   borrowernumber = ?
261                 AND   reservedate    = ?
262             ';
263             my $csth = $dbh->prepare($query);
264             $csth->execute($data->{biblionumber}, $data->{borrowernumber}, $data->{reservedate});
265             my @bibitemno;
266             while ( my $bibitemnos = $csth->fetchrow_array ) {
267                 push( @bibitemno, $bibitemnos );    # FIXME: inefficient: use fetchall_arrayref
268             }
269             my $count = scalar @bibitemno;
270     
271             # if we have two or more different specific itemtypes
272             # reserved by same person on same day
273             my $bdata;
274             if ( $count > 1 ) {
275                 $bdata = GetBiblioItemData( $bibitemno[$i] );   # FIXME: This doesn't make sense.
276                 $i++; #  $i can increase each pass, but the next @bibitemno might be smaller?
277             }
278             else {
279                 # Look up the book we just found.
280                 $bdata = GetBiblioItemData( $bibitemno[0] );
281             }
282             # Add the results of this latest search to the current
283             # results.
284             # FIXME - An 'each' would probably be more efficient.
285             foreach my $key ( keys %$bdata ) {
286                 $data->{$key} = $bdata->{$key};
287             }
288         }
289         push @results, $data;
290     }
291     return ( $#results + 1, \@results );
292 }
293
294 =item GetReservesFromItemnumber
295
296  ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber);
297
298    TODO :: Description here
299
300 =cut
301
302 sub GetReservesFromItemnumber {
303     my ( $itemnumber ) = @_;
304     my $dbh   = C4::Context->dbh;
305     my $query = "
306     SELECT reservedate,borrowernumber,branchcode
307     FROM   reserves
308     WHERE  itemnumber=?
309     ";
310     my $sth_res = $dbh->prepare($query);
311     $sth_res->execute($itemnumber);
312     my ( $reservedate, $borrowernumber,$branchcode ) = $sth_res->fetchrow_array;
313     return ( $reservedate, $borrowernumber, $branchcode );
314 }
315
316 =item GetReservesFromBorrowernumber
317
318     $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
319     
320     TODO :: Descritpion
321     
322 =cut
323
324 sub GetReservesFromBorrowernumber {
325     my ( $borrowernumber, $status ) = @_;
326     my $dbh   = C4::Context->dbh;
327     my $sth;
328     if ($status) {
329         $sth = $dbh->prepare("
330             SELECT *
331             FROM   reserves
332             WHERE  borrowernumber=?
333                 AND found =?
334             ORDER BY reservedate
335         ");
336         $sth->execute($borrowernumber,$status);
337     } else {
338         $sth = $dbh->prepare("
339             SELECT *
340             FROM   reserves
341             WHERE  borrowernumber=?
342             ORDER BY reservedate
343         ");
344         $sth->execute($borrowernumber);
345     }
346     my $data = $sth->fetchall_arrayref({});
347     return @$data;
348 }
349 #-------------------------------------------------------------------------------------
350
351 =item GetReserveCount
352
353 $number = &GetReserveCount($borrowernumber);
354
355 this function returns the number of reservation for a borrower given on input arg.
356
357 =cut
358
359 sub GetReserveCount {
360     my ($borrowernumber) = @_;
361
362     my $dbh = C4::Context->dbh;
363
364     my $query = '
365         SELECT COUNT(*) AS counter
366         FROM reserves
367           WHERE borrowernumber = ?
368     ';
369     my $sth = $dbh->prepare($query);
370     $sth->execute($borrowernumber);
371     my $row = $sth->fetchrow_hashref;
372     return $row->{counter};
373 }
374
375 =item GetOtherReserves
376
377 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
378
379 Check queued list of this document and check if this document must be  transfered
380
381 =cut
382
383 sub GetOtherReserves {
384     my ($itemnumber) = @_;
385     my $messages;
386     my $nextreservinfo;
387     my ( $restype, $checkreserves ) = CheckReserves($itemnumber);
388     if ($checkreserves) {
389         my $iteminfo = GetItem($itemnumber);
390         if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
391             $messages->{'transfert'} = $checkreserves->{'branchcode'};
392             #minus priorities of others reservs
393             ModReserveMinusPriority(
394                 $itemnumber,
395                 $checkreserves->{'borrowernumber'},
396                 $iteminfo->{'biblionumber'}
397             );
398
399             #launch the subroutine dotransfer
400             C4::Items::ModItemTransfer(
401                 $itemnumber,
402                 $iteminfo->{'holdingbranch'},
403                 $checkreserves->{'branchcode'}
404               ),
405               ;
406         }
407
408      #step 2b : case of a reservation on the same branch, set the waiting status
409         else {
410             $messages->{'waiting'} = 1;
411             ModReserveMinusPriority(
412                 $itemnumber,
413                 $checkreserves->{'borrowernumber'},
414                 $iteminfo->{'biblionumber'}
415             );
416             ModReserveStatus($itemnumber,'W');
417         }
418
419         $nextreservinfo = $checkreserves->{'borrowernumber'};
420     }
421
422     return ( $messages, $nextreservinfo );
423 }
424
425 =item GetReserveFee
426
427 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
428
429 Calculate the fee for a reserve
430
431 =cut
432
433 sub GetReserveFee {
434     my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
435
436     #check for issues;
437     my $dbh   = C4::Context->dbh;
438     my $const = lc substr( $constraint, 0, 1 );
439     my $query = qq/
440       SELECT * FROM borrowers
441     LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
442     WHERE borrowernumber = ?
443     /;
444     my $sth = $dbh->prepare($query);
445     $sth->execute($borrowernumber);
446     my $data = $sth->fetchrow_hashref;
447     $sth->finish();
448     my $fee      = $data->{'reservefee'};
449     my $cntitems = @- > $bibitems;
450
451     if ( $fee > 0 ) {
452
453         # check for items on issue
454         # first find biblioitem records
455         my @biblioitems;
456         my $sth1 = $dbh->prepare(
457             "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
458                    WHERE (biblio.biblionumber = ?)"
459         );
460         $sth1->execute($biblionumber);
461         while ( my $data1 = $sth1->fetchrow_hashref ) {
462             if ( $const eq "a" ) {
463                 push @biblioitems, $data1;
464             }
465             else {
466                 my $found = 0;
467                 my $x     = 0;
468                 while ( $x < $cntitems ) {
469                     if ( @$bibitems->{'biblioitemnumber'} ==
470                         $data->{'biblioitemnumber'} )
471                     {
472                         $found = 1;
473                     }
474                     $x++;
475                 }
476                 if ( $const eq 'o' ) {
477                     if ( $found == 1 ) {
478                         push @biblioitems, $data1;
479                     }
480                 }
481                 else {
482                     if ( $found == 0 ) {
483                         push @biblioitems, $data1;
484                     }
485                 }
486             }
487         }
488         $sth1->finish;
489         my $cntitemsfound = @biblioitems;
490         my $issues        = 0;
491         my $x             = 0;
492         my $allissued     = 1;
493         while ( $x < $cntitemsfound ) {
494             my $bitdata = $biblioitems[$x];
495             my $sth2    = $dbh->prepare(
496                 "SELECT * FROM items
497                      WHERE biblioitemnumber = ?"
498             );
499             $sth2->execute( $bitdata->{'biblioitemnumber'} );
500             while ( my $itdata = $sth2->fetchrow_hashref ) {
501                 my $sth3 = $dbh->prepare(
502                     "SELECT * FROM issues
503                        WHERE itemnumber = ?"
504                 );
505                 $sth3->execute( $itdata->{'itemnumber'} );
506                 if ( my $isdata = $sth3->fetchrow_hashref ) {
507                 }
508                 else {
509                     $allissued = 0;
510                 }
511             }
512             $x++;
513         }
514         if ( $allissued == 0 ) {
515             my $rsth =
516               $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
517             $rsth->execute($biblionumber);
518             if ( my $rdata = $rsth->fetchrow_hashref ) {
519             }
520             else {
521                 $fee = 0;
522             }
523         }
524     }
525     return $fee;
526 }
527
528 =item GetReservesToBranch
529
530 @transreserv = GetReservesToBranch( $frombranch );
531
532 Get reserve list for a given branch
533
534 =cut
535
536 sub GetReservesToBranch {
537     my ( $frombranch ) = @_;
538     my $dbh = C4::Context->dbh;
539     my $sth = $dbh->prepare(
540         "SELECT borrowernumber,reservedate,itemnumber,timestamp
541          FROM reserves 
542          WHERE priority='0' 
543            AND branchcode=?"
544     );
545     $sth->execute( $frombranch );
546     my @transreserv;
547     my $i = 0;
548     while ( my $data = $sth->fetchrow_hashref ) {
549         $transreserv[$i] = $data;
550         $i++;
551     }
552     return (@transreserv);
553 }
554
555 =item GetReservesForBranch
556
557 @transreserv = GetReservesForBranch($frombranch);
558
559 =cut
560
561 sub GetReservesForBranch {
562     my ($frombranch) = @_;
563     my $dbh          = C4::Context->dbh;
564         my $query        = "SELECT borrowernumber,reservedate,itemnumber,waitingdate
565         FROM   reserves 
566         WHERE   priority='0'
567             AND found='W' ";
568     if ($frombranch){
569         $query .= " AND branchcode=? ";
570         }
571     $query .= "ORDER BY waitingdate" ;
572     my $sth = $dbh->prepare($query);
573     if ($frombranch){
574                 $sth->execute($frombranch);
575         }
576     else {
577                 $sth->execute();
578         }
579     my @transreserv;
580     my $i = 0;
581     while ( my $data = $sth->fetchrow_hashref ) {
582         $transreserv[$i] = $data;
583         $i++;
584     }
585     return (@transreserv);
586 }
587
588 =item CheckReserves
589
590   ($status, $reserve) = &CheckReserves($itemnumber);
591
592 Find a book in the reserves.
593
594 C<$itemnumber> is the book's item number.
595
596 As I understand it, C<&CheckReserves> looks for the given item in the
597 reserves. If it is found, that's a match, and C<$status> is set to
598 C<Waiting>.
599
600 Otherwise, it finds the most important item in the reserves with the
601 same biblio number as this book (I'm not clear on this) and returns it
602 with C<$status> set to C<Reserved>.
603
604 C<&CheckReserves> returns a two-element list:
605
606 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
607
608 C<$reserve> is the reserve item that matched. It is a
609 reference-to-hash whose keys are mostly the fields of the reserves
610 table in the Koha database.
611
612 =cut
613
614 sub CheckReserves {
615     my ( $item, $barcode ) = @_;
616     my $dbh = C4::Context->dbh;
617     my $sth;
618     if ($item) {
619         my $qitem = $dbh->quote($item);
620         # Look up the item by itemnumber
621         my $query = "
622             SELECT items.biblionumber, items.biblioitemnumber, itemtypes.notforloan, items.notforloan AS itemnotforloan
623             FROM   items
624             LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
625             LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
626             WHERE  itemnumber=$qitem
627         ";
628         $sth = $dbh->prepare($query);
629     }
630     else {
631         my $qbc = $dbh->quote($barcode);
632         # Look up the item by barcode
633         my $query = "
634             SELECT items.biblionumber, items.biblioitemnumber, itemtypes.notforloan, items.notforloan AS itemnotforloan
635             FROM   items
636             LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
637             LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
638             WHERE  items.biblioitemnumber = biblioitems.biblioitemnumber
639               AND biblioitems.itemtype = itemtypes.itemtype
640               AND barcode=$qbc
641         ";
642         $sth = $dbh->prepare($query);
643
644         # FIXME - This function uses $item later on. Ought to set it here.
645     }
646     $sth->execute;
647     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item ) = $sth->fetchrow_array;
648     $sth->finish;
649     # if item is not for loan it cannot be reserved either.....
650     #    execption to notforloan is where items.notforloan < 0 :  This indicates the item is holdable. 
651     return ( 0, 0 ) if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
652
653     # get the reserves...
654     # Find this item in the reserves
655     my @reserves = _Findgroupreserve( $bibitem, $biblio, $item );
656     my $count    = scalar @reserves;
657
658     # $priority and $highest are used to find the most important item
659     # in the list returned by &_Findgroupreserve. (The lower $priority,
660     # the more important the item.)
661     # $highest is the most important item we've seen so far.
662     my $priority = 10000000;
663     my $highest;
664     if ($count) {
665         foreach my $res (@reserves) {
666             # FIXME - $item might be undefined or empty: the caller
667             # might be searching by barcode.
668             if ( $res->{'itemnumber'} == $item && $res->{'priority'} == 0) {
669                 # Found it
670                 return ( "Waiting", $res );
671             }
672             else {
673                 # See if this item is more important than what we've got
674                 # so far.
675                 if ( $res->{'priority'} != 0 && $res->{'priority'} < $priority )
676                 {
677                     $priority = $res->{'priority'};
678                     $highest  = $res;
679                 }
680             }
681         }
682     }
683
684     # If we get this far, then no exact match was found. Print the
685     # most important item on the list. I think this tells us who's
686     # next in line to get this book.
687     if ($highest) {    # FIXME - $highest might be undefined
688         $highest->{'itemnumber'} = $item;
689         return ( "Reserved", $highest );
690     }
691     else {
692         return ( 0, 0 );
693     }
694 }
695
696 =item CancelReserve
697
698   &CancelReserve($biblionumber, $itemnumber, $borrowernumber);
699
700 Cancels a reserve.
701
702 Use either C<$biblionumber> or C<$itemnumber> to specify the item to
703 cancel, but not both: if both are given, C<&CancelReserve> does
704 nothing.
705
706 C<$borrowernumber> is the borrower number of the patron on whose
707 behalf the book was reserved.
708
709 If C<$biblionumber> was given, C<&CancelReserve> also adjusts the
710 priorities of the other people who are waiting on the book.
711
712 =cut
713
714 sub CancelReserve {
715     my ( $biblio, $item, $borr ) = @_;
716     my $dbh = C4::Context->dbh;
717         if ( $item and $borr ) {
718         # removing a waiting reserve record....
719         # update the database...
720         my $query = "
721             UPDATE reserves
722             SET    cancellationdate = now(),
723                    found            = Null,
724                    priority         = 0
725             WHERE  itemnumber       = ?
726              AND   borrowernumber   = ?
727         ";
728         my $sth = $dbh->prepare($query);
729         $sth->execute( $item, $borr );
730         $sth->finish;
731         $query = "
732             INSERT INTO old_reserves
733             SELECT * FROM reserves
734             WHERE  itemnumber       = ?
735              AND   borrowernumber   = ?
736         ";
737         $sth = $dbh->prepare($query);
738         $sth->execute( $item, $borr );
739         $query = "
740             DELETE FROM reserves
741             WHERE  itemnumber       = ?
742              AND   borrowernumber   = ?
743         ";
744         $sth = $dbh->prepare($query);
745         $sth->execute( $item, $borr );
746     }
747     else {
748         # removing a reserve record....
749         # get the prioritiy on this record....
750         my $priority;
751         my $query = qq/
752             SELECT priority FROM reserves
753             WHERE biblionumber   = ?
754               AND borrowernumber = ?
755               AND cancellationdate IS NULL
756               AND itemnumber IS NULL
757         /;
758         my $sth = $dbh->prepare($query);
759         $sth->execute( $biblio, $borr );
760         ($priority) = $sth->fetchrow_array;
761         $sth->finish;
762         $query = qq/
763             UPDATE reserves
764             SET    cancellationdate = now(),
765                    found            = Null,
766                    priority         = 0
767             WHERE  biblionumber     = ?
768               AND  borrowernumber   = ?
769         /;
770
771         # update the database, removing the record...
772         $sth = $dbh->prepare($query);
773         $sth->execute( $biblio, $borr );
774         $sth->finish;
775
776         $query = qq/
777             INSERT INTO old_reserves
778             SELECT * FROM reserves
779             WHERE  biblionumber     = ?
780               AND  borrowernumber   = ?
781         /;
782         $sth = $dbh->prepare($query);
783         $sth->execute( $biblio, $borr );
784
785         $query = qq/
786             DELETE FROM reserves
787             WHERE  biblionumber     = ?
788               AND  borrowernumber   = ?
789         /;
790         $sth = $dbh->prepare($query);
791         $sth->execute( $biblio, $borr );
792
793         # now fix the priority on the others....
794         _FixPriority( $priority, $biblio );
795     }
796 }
797
798 =item ModReserve
799
800 =over 4
801
802 ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber])
803
804 =back
805
806 Change a hold request's priority or cancel it.
807
808 C<$rank> specifies the effect of the change.  If C<$rank>
809 is 'W' or 'n', nothing happens.  This corresponds to leaving a
810 request alone when changing its priority in the holds queue
811 for a bib.
812
813 If C<$rank> is 'del', the hold request is cancelled.
814
815 If C<$rank> is an integer greater than zero, the priority of
816 the request is set to that value.  Since priority != 0 means
817 that the item is not waiting on the hold shelf, setting the 
818 priority to a non-zero value also sets the request's found
819 status and waiting date to NULL. 
820
821 The optional C<$itemnumber> parameter is used only when
822 C<$rank> is a non-zero integer; if supplied, the itemnumber 
823 of the hold request is set accordingly; if omitted, the itemnumber
824 is cleared.
825
826 FIXME: Note that the forgoing can have the effect of causing
827 item-level hold requests to turn into title-level requests.  This
828 will be fixed once reserves has separate columns for requested
829 itemnumber and supplying itemnumber.
830
831 =cut
832
833 sub ModReserve {
834     #subroutine to update a reserve
835     my ( $rank, $biblio, $borrower, $branch , $itemnumber) = @_;
836      return if $rank eq "W";
837      return if $rank eq "n";
838     my $dbh = C4::Context->dbh;
839     if ( $rank eq "del" ) {
840         my $query = qq/
841             UPDATE reserves
842             SET    cancellationdate=now()
843             WHERE  biblionumber   = ?
844              AND   borrowernumber = ?
845         /;
846         my $sth = $dbh->prepare($query);
847         $sth->execute( $biblio, $borrower );
848         $sth->finish;
849         $query = qq/
850             INSERT INTO old_reserves
851             SELECT *
852             FROM   reserves 
853             WHERE  biblionumber   = ?
854              AND   borrowernumber = ?
855         /;
856         $sth = $dbh->prepare($query);
857         $sth->execute( $biblio, $borrower );
858         $query = qq/
859             DELETE FROM reserves 
860             WHERE  biblionumber   = ?
861              AND   borrowernumber = ?
862         /;
863         $sth = $dbh->prepare($query);
864         $sth->execute( $biblio, $borrower );
865         
866     }
867     elsif ($rank =~ /^\d+/ and $rank > 0) {
868         my $query = qq/
869         UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
870             WHERE biblionumber   = ?
871              AND borrowernumber = ?
872         /;
873         my $sth = $dbh->prepare($query);
874         $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower);
875         $sth->finish;
876         _FixPriority( $biblio, $borrower, $rank);
877     }
878 }
879
880 =item ModReserveFill
881
882   &ModReserveFill($reserve);
883
884 Fill a reserve. If I understand this correctly, this means that the
885 reserved book has been found and given to the patron who reserved it.
886
887 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
888 whose keys are fields from the reserves table in the Koha database.
889
890 =cut
891
892 sub ModReserveFill {
893     my ($res) = @_;
894     my $dbh = C4::Context->dbh;
895     # fill in a reserve record....
896     my $biblionumber = $res->{'biblionumber'};
897     my $borrowernumber    = $res->{'borrowernumber'};
898     my $resdate = $res->{'reservedate'};
899
900     # get the priority on this record....
901     my $priority;
902     my $query = "SELECT priority
903                  FROM   reserves
904                  WHERE  biblionumber   = ?
905                   AND   borrowernumber = ?
906                   AND   reservedate    = ?";
907     my $sth = $dbh->prepare($query);
908     $sth->execute( $biblionumber, $borrowernumber, $resdate );
909     ($priority) = $sth->fetchrow_array;
910     $sth->finish;
911
912     # update the database...
913     $query = "UPDATE reserves
914                   SET    found            = 'F',
915                          priority         = 0
916                  WHERE  biblionumber     = ?
917                     AND reservedate      = ?
918                     AND borrowernumber   = ?
919                 ";
920     $sth = $dbh->prepare($query);
921     $sth->execute( $biblionumber, $resdate, $borrowernumber );
922     $sth->finish;
923
924     # move to old_reserves
925     $query = "INSERT INTO old_reserves
926                  SELECT * FROM reserves
927                  WHERE  biblionumber     = ?
928                     AND reservedate      = ?
929                     AND borrowernumber   = ?
930                 ";
931     $sth = $dbh->prepare($query);
932     $sth->execute( $biblionumber, $resdate, $borrowernumber );
933     $query = "DELETE FROM reserves
934                  WHERE  biblionumber     = ?
935                     AND reservedate      = ?
936                     AND borrowernumber   = ?
937                 ";
938     $sth = $dbh->prepare($query);
939     $sth->execute( $biblionumber, $resdate, $borrowernumber );
940     
941     # now fix the priority on the others (if the priority wasn't
942     # already sorted!)....
943     unless ( $priority == 0 ) {
944         _FixPriority( $priority, $biblionumber );
945     }
946 }
947
948 =item ModReserveStatus
949
950 &ModReserveStatus($itemnumber, $newstatus);
951
952 Update the reserve status for the active (priority=0) reserve.
953
954 $itemnumber is the itemnumber the reserve is on
955
956 $newstatus is the new status.
957
958 =cut
959
960 sub ModReserveStatus {
961
962     #first : check if we have a reservation for this item .
963     my ($itemnumber, $newstatus) = @_;
964     my $dbh          = C4::Context->dbh;
965     my $query = " UPDATE reserves
966     SET    found=?,waitingdate = now()
967     WHERE itemnumber=?
968       AND found IS NULL
969       AND priority = 0
970     ";
971     my $sth_set = $dbh->prepare($query);
972     $sth_set->execute( $newstatus, $itemnumber );
973 }
974
975 =item ModReserveAffect
976
977 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
978
979 This function affect an item and a status for a given reserve
980 The itemnumber parameter is used to find the biblionumber.
981 with the biblionumber & the borrowernumber, we can affect the itemnumber
982 to the correct reserve.
983
984 if $transferToDo is not set, then the status is set to "Waiting" as well.
985 otherwise, a transfer is on the way, and the end of the transfer will 
986 take care of the waiting status
987 =cut
988
989 sub ModReserveAffect {
990     my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
991     my $dbh = C4::Context->dbh;
992
993     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
994     # attached to $itemnumber
995     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
996     $sth->execute($itemnumber);
997     my ($biblionumber) = $sth->fetchrow;
998     # If we affect a reserve that has to be transfered, don't set to Waiting
999     my $query;
1000     if ($transferToDo) {
1001     $query = "
1002         UPDATE reserves
1003         SET    priority = 0,
1004                itemnumber = ?
1005         WHERE borrowernumber = ?
1006           AND biblionumber = ?
1007     ";
1008     }
1009     else {
1010     # affect the reserve to Waiting as well.
1011     $query = "
1012         UPDATE reserves
1013         SET     priority = 0,
1014                 found = 'W',
1015                 waitingdate=now(),
1016                 itemnumber = ?
1017         WHERE borrowernumber = ?
1018           AND biblionumber = ?
1019     ";
1020     }
1021     $sth = $dbh->prepare($query);
1022     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1023     $sth->finish;
1024     
1025     _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo );
1026
1027     return;
1028 }
1029
1030 =item ModReserveCancelAll
1031
1032 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1033
1034     function to cancel reserv,check other reserves, and transfer document if it's necessary
1035
1036 =cut
1037
1038 sub ModReserveCancelAll {
1039     my $messages;
1040     my $nextreservinfo;
1041     my ( $itemnumber, $borrowernumber ) = @_;
1042
1043     #step 1 : cancel the reservation
1044     my $CancelReserve = CancelReserve( undef, $itemnumber, $borrowernumber );
1045
1046     #step 2 launch the subroutine of the others reserves
1047     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1048
1049     return ( $messages, $nextreservinfo );
1050 }
1051
1052 =item ModReserveMinusPriority
1053
1054 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1055
1056 Reduce the values of queuded list     
1057
1058 =cut
1059
1060 sub ModReserveMinusPriority {
1061     my ( $itemnumber, $borrowernumber, $biblionumber ) = @_;
1062
1063     #first step update the value of the first person on reserv
1064     my $dbh   = C4::Context->dbh;
1065     my $query = "
1066         UPDATE reserves
1067         SET    priority = 0 , itemnumber = ? 
1068         WHERE  borrowernumber=?
1069           AND  biblionumber=?
1070     ";
1071     my $sth_upd = $dbh->prepare($query);
1072     $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber );
1073     # second step update all others reservs
1074     _FixPriority($biblionumber, $borrowernumber, '0');
1075 }
1076
1077 =item GetReserveInfo
1078
1079 &GetReserveInfo($borrowernumber,$biblionumber);
1080
1081  Get item and borrower details for a current hold.
1082  Current implementation this query should have a single result.
1083 =cut
1084
1085 sub GetReserveInfo {
1086         my ( $borrowernumber, $biblionumber ) = @_;
1087     my $dbh = C4::Context->dbh;
1088         my $strsth="SELECT reservedate, reservenotes, reserves.borrowernumber,
1089                                 reserves.biblionumber, reserves.branchcode,
1090                                 notificationdate, reminderdate, priority, found,
1091                                 firstname, surname, phone, 
1092                                 email, address, address2,
1093                                 cardnumber, city, zipcode,
1094                                 biblio.title, biblio.author,
1095                                 items.holdingbranch, items.itemcallnumber, items.itemnumber, 
1096                                 barcode, notes
1097                         FROM reserves left join items 
1098                                 ON items.itemnumber=reserves.itemnumber , 
1099                                 borrowers, biblio 
1100                         WHERE 
1101                                 reserves.borrowernumber=?  &&
1102                                 reserves.biblionumber=? && 
1103                                 reserves.borrowernumber=borrowers.borrowernumber && 
1104                                 reserves.biblionumber=biblio.biblionumber ";
1105         my $sth = $dbh->prepare($strsth); 
1106         $sth->execute($borrowernumber,$biblionumber);
1107
1108         my $data = $sth->fetchrow_hashref;
1109         return $data;
1110
1111 }
1112
1113 =item IsAvailableForItemLevelRequest
1114
1115 =over 4
1116
1117 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1118
1119 =back
1120
1121 Checks whether a given item record is available for an
1122 item-level hold request.  An item is available if
1123
1124 * it is not lost AND 
1125 * it is not damaged AND 
1126 * it is not withdrawn AND 
1127 * does not have a not for loan value > 0
1128
1129 Whether or not the item is currently on loan is 
1130 also checked - if the AllowOnShelfHolds system preference
1131 is ON, an item can be requested even if it is currently
1132 on loan to somebody else.  If the system preference
1133 is OFF, an item that is currently checked out cannot
1134 be the target of an item-level hold request.
1135
1136 Note that IsAvailableForItemLevelRequest() does not
1137 check if the staff operator is authorized to place
1138 a request on the item - in particular,
1139 this routine does not check IndependantBranches
1140 and canreservefromotherbranches.
1141
1142 =cut
1143
1144 sub IsAvailableForItemLevelRequest {
1145     my $itemnumber = shift;
1146    
1147     my $item = GetItem($itemnumber);
1148
1149     # must check the notforloan setting of the itemtype
1150     # FIXME - a lot of places in the code do this
1151     #         or something similar - need to be
1152     #         consolidated
1153     my $dbh = C4::Context->dbh;
1154     my $notforloan_query;
1155     if (C4::Context->preference('item-level_itypes')) {
1156         $notforloan_query = "SELECT itemtypes.notforloan
1157                              FROM items
1158                              JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1159                              WHERE itemnumber = ?";
1160     } else {
1161         $notforloan_query = "SELECT itemtypes.notforloan
1162                              FROM items
1163                              JOIN biblioitems USING (biblioitemnumber)
1164                              JOIN itemtypes USING (itemtype)
1165                              WHERE itemnumber = ?";
1166     }
1167     my $sth = $dbh->prepare($notforloan_query);
1168     $sth->execute($itemnumber);
1169     my $notforloan_per_itemtype = 0;
1170     if (my ($notforloan) = $sth->fetchrow_array) {
1171         $notforloan_per_itemtype = 1 if $notforloan;
1172     }
1173
1174     my $available_per_item = 1;
1175     $available_per_item = 0 if $item->{itemlost} or
1176                                ( $item->{notforloan} > 0 ) or
1177                                ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1178                                $item->{wthdrawn} or
1179                                $notforloan_per_itemtype;
1180
1181     if (C4::Context->preference('AllowOnShelfHolds')) {
1182         return $available_per_item;
1183     } else {
1184         return ($available_per_item and $item->{onloan}); 
1185     }
1186 }
1187
1188 =item _FixPriority
1189
1190 &_FixPriority($biblio,$borrowernumber,$rank);
1191
1192  Only used internally (so don't export it)
1193  Changed how this functions works #
1194  Now just gets an array of reserves in the rank order and updates them with
1195  the array index (+1 as array starts from 0)
1196  and if $rank is supplied will splice item from the array and splice it back in again
1197  in new priority rank
1198
1199 =cut 
1200
1201 sub _FixPriority {
1202     my ( $biblio, $borrowernumber, $rank ) = @_;
1203     my $dbh = C4::Context->dbh;
1204      if ( $rank eq "del" ) {
1205          CancelReserve( $biblio, undef, $borrowernumber );
1206      }
1207     if ( $rank eq "W" || $rank eq "0" ) {
1208
1209         # make sure priority for waiting items is 0
1210         my $query = qq/
1211             UPDATE reserves
1212             SET    priority = 0
1213             WHERE biblionumber = ?
1214               AND borrowernumber = ?
1215               AND found ='W'
1216         /;
1217         my $sth = $dbh->prepare($query);
1218         $sth->execute( $biblio, $borrowernumber );
1219     }
1220     my @priority;
1221     my @reservedates;
1222
1223     # get whats left
1224 # FIXME adding a new security in returned elements for changing priority,
1225 # now, we don't care anymore any reservations with itemnumber linked (suppose a waiting reserve)
1226         # This is wrong a waiting reserve has W set
1227         # The assumption that having an itemnumber set means waiting is wrong and should be corrected any place it occurs
1228     my $query = qq/
1229         SELECT borrowernumber, reservedate, constrainttype
1230         FROM   reserves
1231         WHERE  biblionumber   = ?
1232           AND  ((found <> 'W') or found is NULL)
1233         ORDER BY priority ASC
1234     /;
1235     my $sth = $dbh->prepare($query);
1236     $sth->execute($biblio);
1237     while ( my $line = $sth->fetchrow_hashref ) {
1238         push( @reservedates, $line );
1239         push( @priority,     $line );
1240     }
1241
1242     # To find the matching index
1243     my $i;
1244     my $key = -1;    # to allow for 0 to be a valid result
1245     for ( $i = 0 ; $i < @priority ; $i++ ) {
1246         if ( $borrowernumber == $priority[$i]->{'borrowernumber'} ) {
1247             $key = $i;    # save the index
1248             last;
1249         }
1250     }
1251
1252     # if index exists in array then move it to new position
1253     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1254         my $new_rank = $rank -
1255           1;    # $new_rank is what you want the new index to be in the array
1256         my $moving_item = splice( @priority, $key, 1 );
1257         splice( @priority, $new_rank, 0, $moving_item );
1258     }
1259
1260     # now fix the priority on those that are left....
1261     $query = "
1262             UPDATE reserves
1263             SET    priority = ?
1264                 WHERE  biblionumber = ?
1265                  AND borrowernumber   = ?
1266                  AND reservedate = ?
1267          AND found IS NULL
1268     ";
1269     $sth = $dbh->prepare($query);
1270     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1271         $sth->execute(
1272             $j + 1, $biblio,
1273             $priority[$j]->{'borrowernumber'},
1274             $priority[$j]->{'reservedate'}
1275         );
1276         $sth->finish;
1277     }
1278 }
1279
1280 =item _Findgroupreserve
1281
1282   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1283
1284 Looks for an item-specific match first, then for a title-level match, returning the
1285 first match found.  If neither, then we look for a 3rd kind of match based on
1286 reserve constraints.
1287
1288 TODO: add more explanation about reserve constraints
1289
1290 C<&_Findgroupreserve> returns :
1291 C<@results> is an array of references-to-hash whose keys are mostly
1292 fields from the reserves table of the Koha database, plus
1293 C<biblioitemnumber>.
1294
1295 =cut
1296
1297 sub _Findgroupreserve {
1298     my ( $bibitem, $biblio, $itemnumber ) = @_;
1299     my $dbh   = C4::Context->dbh;
1300
1301     # check for exact targetted match
1302         # This select is valid for both item_level and biblio_level
1303     my $item_level_target_query = qq/
1304         SELECT reserves.biblionumber        AS biblionumber,
1305                reserves.borrowernumber      AS borrowernumber,
1306                reserves.reservedate         AS reservedate,
1307                reserves.branchcode          AS branchcode,
1308                reserves.cancellationdate    AS cancellationdate,
1309                reserves.found               AS found,
1310                reserves.reservenotes        AS reservenotes,
1311                reserves.priority            AS priority,
1312                reserves.timestamp           AS timestamp,
1313                biblioitems.biblioitemnumber AS biblioitemnumber,
1314                reserves.itemnumber          AS itemnumber
1315         FROM reserves
1316         JOIN biblioitems USING (biblionumber)
1317         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1318         WHERE found IS NULL
1319         AND priority > 0
1320         AND holds_fill_targets.itemnumber = ?
1321
1322     /;
1323     my $sth = $dbh->prepare($item_level_target_query);
1324     $sth->execute($itemnumber);
1325         my $data = $sth->fetchall_arrayref({});
1326     return @$data if ($data);
1327
1328     my $query = qq/
1329         SELECT reserves.biblionumber               AS biblionumber,
1330                reserves.borrowernumber             AS borrowernumber,
1331                reserves.reservedate                AS reservedate,
1332                reserves.branchcode                 AS branchcode,
1333                reserves.cancellationdate           AS cancellationdate,
1334                reserves.found                      AS found,
1335                reserves.reservenotes               AS reservenotes,
1336                reserves.priority                   AS priority,
1337                reserves.timestamp                  AS timestamp,
1338                reserveconstraints.biblioitemnumber AS biblioitemnumber,
1339                reserves.itemnumber                 AS itemnumber
1340         FROM reserves
1341           LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1342         WHERE reserves.biblionumber = ?
1343           AND ( ( reserveconstraints.biblioitemnumber = ?
1344           AND reserves.borrowernumber = reserveconstraints.borrowernumber
1345           AND reserves.reservedate    = reserveconstraints.reservedate )
1346           OR  reserves.constrainttype='a' )
1347           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1348     /;
1349     $sth = $dbh->prepare($query);
1350     $sth->execute( $biblio, $bibitem, $itemnumber );
1351     $data = $sth->fetchall_arrayref({});
1352     return @$data if ($data);
1353         return undef;
1354 }
1355
1356 =item _koha_notify_reserve
1357
1358 =over 4
1359
1360 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1361
1362 =back
1363
1364 Sends a notification to the patron that their hold has been filled (through
1365 ModReserveAffect, _not_ ModReserveFill)
1366
1367 =cut
1368
1369 sub _koha_notify_reserve {
1370     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1371
1372     my $dbh = C4::Context->dbh;
1373     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold Filled' } );
1374
1375     return if ( !defined( $messagingprefs->{'letter_code'} ) );
1376
1377     my $sth = $dbh->prepare("
1378         SELECT *
1379         FROM   reserves
1380         WHERE  borrowernumber = ?
1381             AND biblionumber = ?
1382     ");
1383     $sth->execute( $borrowernumber, $biblionumber );
1384     my $reserve = $sth->fetchrow_hashref;
1385     my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1386
1387     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1388
1389     my $letter = getletter( 'reserves', $messagingprefs->{'letter_code'} );
1390
1391     C4::Letters::parseletter( $letter, 'branches', $reserve->{'branchcode'} );
1392     C4::Letters::parseletter( $letter, 'borrowers', $reserve->{'borrowernumber'} );
1393     C4::Letters::parseletter( $letter, 'biblio', $reserve->{'biblionumber'} );
1394     C4::Letters::parseletter( $letter, 'reserves', $reserve->{'borrowernumber'}, $reserve->{'biblionumber'} );
1395
1396     if ( $reserve->{'itemnumber'} ) {
1397         C4::Letters::parseletter( $letter, 'items', $reserve->{'itemnumber'} );
1398     }
1399     $letter->{'content'} =~ s/<<[a-z0-9_]+\.[a-z0-9]+>>//g; #remove any stragglers
1400
1401     if ( -1 !=  firstidx { $_ eq 'email' } @{$messagingprefs->{transports}} ) {
1402         # aka, 'email' in ->{'transports'}
1403         C4::Letters::EnqueueLetter(
1404             {   letter                 => $letter,
1405                 borrowernumber         => $borrowernumber,
1406                 message_transport_type => 'email',
1407                 from_address           => $admin_email_address,
1408             }
1409         );
1410     }
1411
1412     if ( -1 != firstidx { $_ eq 'sms' } @{$messagingprefs->{transports}} ) {
1413         C4::Letters::EnqueueLetter(
1414             {   letter                 => $letter,
1415                 borrowernumber         => $borrowernumber,
1416                 message_transport_type => 'sms',
1417             }
1418         );
1419     }
1420 }
1421
1422 =back
1423
1424 =head1 AUTHOR
1425
1426 Koha Developement team <info@koha.org>
1427
1428 =cut
1429
1430 1;