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