Minor bug fix Documentation update : 3039
[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         if ($data->{constrainttype} eq 'o') {
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         }
267         push @results, $data;
268     }
269     return ( $#results + 1, \@results );
270 }
271
272 =item GetReservesFromItemnumber
273
274  ( $reservedate, $borrowernumber, $branchcode ) = GetReservesFromItemnumber($itemnumber);
275
276    TODO :: Description here
277
278 =cut
279
280 sub GetReservesFromItemnumber {
281     my ( $itemnumber ) = @_;
282     my $dbh   = C4::Context->dbh;
283     my $query = "
284     SELECT reservedate,borrowernumber,branchcode
285     FROM   reserves
286     WHERE  itemnumber=?
287     ";
288     my $sth_res = $dbh->prepare($query);
289     $sth_res->execute($itemnumber);
290     my ( $reservedate, $borrowernumber,$branchcode ) = $sth_res->fetchrow_array;
291     return ( $reservedate, $borrowernumber, $branchcode );
292 }
293
294 =item GetReservesFromBorrowernumber
295
296     $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
297     
298     TODO :: Descritpion
299     
300 =cut
301
302 sub GetReservesFromBorrowernumber {
303     my ( $borrowernumber, $status ) = @_;
304     my $dbh   = C4::Context->dbh;
305     my $sth;
306     if ($status) {
307         $sth = $dbh->prepare("
308             SELECT *
309             FROM   reserves
310             WHERE  borrowernumber=?
311                 AND found =?
312             ORDER BY reservedate
313         ");
314         $sth->execute($borrowernumber,$status);
315     } else {
316         $sth = $dbh->prepare("
317             SELECT *
318             FROM   reserves
319             WHERE  borrowernumber=?
320             ORDER BY reservedate
321         ");
322         $sth->execute($borrowernumber);
323     }
324     my $data = $sth->fetchall_arrayref({});
325     return @$data;
326 }
327 #-------------------------------------------------------------------------------------
328
329 =item GetReserveCount
330
331 $number = &GetReserveCount($borrowernumber);
332
333 this function returns the number of reservation for a borrower given on input arg.
334
335 =cut
336
337 sub GetReserveCount {
338     my ($borrowernumber) = @_;
339
340     my $dbh = C4::Context->dbh;
341
342     my $query = '
343         SELECT COUNT(*) AS counter
344         FROM reserves
345           WHERE borrowernumber = ?
346     ';
347     my $sth = $dbh->prepare($query);
348     $sth->execute($borrowernumber);
349     my $row = $sth->fetchrow_hashref;
350     return $row->{counter};
351 }
352
353 =item GetOtherReserves
354
355 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
356
357 Check queued list of this document and check if this document must be  transfered
358
359 =cut
360
361 sub GetOtherReserves {
362     my ($itemnumber) = @_;
363     my $messages;
364     my $nextreservinfo;
365     my ( $restype, $checkreserves ) = CheckReserves($itemnumber);
366     if ($checkreserves and $restype ne "Waiting" ) {
367         my $iteminfo = GetItem($itemnumber);
368         if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
369             $messages->{'transfert'} = $checkreserves->{'branchcode'};
370             #minus priorities of others reservs
371             ModReserveMinusPriority(
372                 $itemnumber,
373                 $checkreserves->{'borrowernumber'},
374                 $iteminfo->{'biblionumber'}
375             );
376
377             #launch the subroutine dotransfer
378             C4::Circulation::ModItemTransfer(
379                 $itemnumber,
380                 $iteminfo->{'holdingbranch'},
381                 $checkreserves->{'branchcode'}
382               ),
383               ;
384         }
385
386      #step 2b : case of a reservation on the same branch, set the waiting status
387         else {
388             $messages->{'waiting'} = 1;
389             ModReserveMinusPriority(
390                 $itemnumber,
391                 $checkreserves->{'borrowernumber'},
392                 $iteminfo->{'biblionumber'}
393             );
394             ModReserveStatus($itemnumber,'W');
395         }
396
397         $nextreservinfo = $checkreserves->{'borrowernumber'};
398     }
399
400     return ( $messages, $nextreservinfo );
401 }
402
403 =item GetReserveFee
404
405 $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
406
407 Calculate the fee for a reserve
408
409 =cut
410
411 sub GetReserveFee {
412     my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
413
414     #check for issues;
415     my $dbh   = C4::Context->dbh;
416     my $const = lc substr( $constraint, 0, 1 );
417     my $query = qq/
418       SELECT * FROM borrowers
419     LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
420     WHERE borrowernumber = ?
421     /;
422     my $sth = $dbh->prepare($query);
423     $sth->execute($borrowernumber);
424     my $data = $sth->fetchrow_hashref;
425     $sth->finish();
426     my $fee      = $data->{'reservefee'};
427     my $cntitems = @- > $bibitems;
428
429     if ( $fee > 0 ) {
430
431         # check for items on issue
432         # first find biblioitem records
433         my @biblioitems;
434         my $sth1 = $dbh->prepare(
435             "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
436                    WHERE (biblio.biblionumber = ?)"
437         );
438         $sth1->execute($biblionumber);
439         while ( my $data1 = $sth1->fetchrow_hashref ) {
440             if ( $const eq "a" ) {
441                 push @biblioitems, $data1;
442             }
443             else {
444                 my $found = 0;
445                 my $x     = 0;
446                 while ( $x < $cntitems ) {
447                     if ( @$bibitems->{'biblioitemnumber'} ==
448                         $data->{'biblioitemnumber'} )
449                     {
450                         $found = 1;
451                     }
452                     $x++;
453                 }
454                 if ( $const eq 'o' ) {
455                     if ( $found == 1 ) {
456                         push @biblioitems, $data1;
457                     }
458                 }
459                 else {
460                     if ( $found == 0 ) {
461                         push @biblioitems, $data1;
462                     }
463                 }
464             }
465         }
466         $sth1->finish;
467         my $cntitemsfound = @biblioitems;
468         my $issues        = 0;
469         my $x             = 0;
470         my $allissued     = 1;
471         while ( $x < $cntitemsfound ) {
472             my $bitdata = $biblioitems[$x];
473             my $sth2    = $dbh->prepare(
474                 "SELECT * FROM items
475                      WHERE biblioitemnumber = ?"
476             );
477             $sth2->execute( $bitdata->{'biblioitemnumber'} );
478             while ( my $itdata = $sth2->fetchrow_hashref ) {
479                 my $sth3 = $dbh->prepare(
480                     "SELECT * FROM issues
481                        WHERE itemnumber = ?"
482                 );
483                 $sth3->execute( $itdata->{'itemnumber'} );
484                 if ( my $isdata = $sth3->fetchrow_hashref ) {
485                 }
486                 else {
487                     $allissued = 0;
488                 }
489             }
490             $x++;
491         }
492         if ( $allissued == 0 ) {
493             my $rsth =
494               $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
495             $rsth->execute($biblionumber);
496             if ( my $rdata = $rsth->fetchrow_hashref ) {
497             }
498             else {
499                 $fee = 0;
500             }
501         }
502     }
503     return $fee;
504 }
505
506 =item GetReservesToBranch
507
508 @transreserv = GetReservesToBranch( $frombranch );
509
510 Get reserve list for a given branch
511
512 =cut
513
514 sub GetReservesToBranch {
515     my ( $frombranch ) = @_;
516     my $dbh = C4::Context->dbh;
517     my $sth = $dbh->prepare(
518         "SELECT borrowernumber,reservedate,itemnumber,timestamp
519          FROM reserves 
520          WHERE priority='0' 
521            AND branchcode=?"
522     );
523     $sth->execute( $frombranch );
524     my @transreserv;
525     my $i = 0;
526     while ( my $data = $sth->fetchrow_hashref ) {
527         $transreserv[$i] = $data;
528         $i++;
529     }
530     return (@transreserv);
531 }
532
533 =item GetReservesForBranch
534
535 @transreserv = GetReservesForBranch($frombranch);
536
537 =cut
538
539 sub GetReservesForBranch {
540     my ($frombranch) = @_;
541     my $dbh          = C4::Context->dbh;
542         my $query        = "SELECT borrowernumber,reservedate,itemnumber,waitingdate
543         FROM   reserves 
544         WHERE   priority='0'
545             AND found='W' ";
546     if ($frombranch){
547         $query .= " AND branchcode=? ";
548         }
549     $query .= "ORDER BY waitingdate" ;
550     my $sth = $dbh->prepare($query);
551     if ($frombranch){
552                 $sth->execute($frombranch);
553         }
554     else {
555                 $sth->execute();
556         }
557     my @transreserv;
558     my $i = 0;
559     while ( my $data = $sth->fetchrow_hashref ) {
560         $transreserv[$i] = $data;
561         $i++;
562     }
563     return (@transreserv);
564 }
565
566 =item CheckReserves
567
568   ($status, $reserve) = &CheckReserves($itemnumber);
569
570 Find a book in the reserves.
571
572 C<$itemnumber> is the book's item number.
573
574 As I understand it, C<&CheckReserves> looks for the given item in the
575 reserves. If it is found, that's a match, and C<$status> is set to
576 C<Waiting>.
577
578 Otherwise, it finds the most important item in the reserves with the
579 same biblio number as this book (I'm not clear on this) and returns it
580 with C<$status> set to C<Reserved>.
581
582 C<&CheckReserves> returns a two-element list:
583
584 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
585
586 C<$reserve> is the reserve item that matched. It is a
587 reference-to-hash whose keys are mostly the fields of the reserves
588 table in the Koha database.
589
590 =cut
591
592 sub CheckReserves {
593     my ( $item, $barcode ) = @_;
594     my $dbh = C4::Context->dbh;
595     my $sth;
596     if ($item) {
597         my $qitem = $dbh->quote($item);
598         # Look up the item by itemnumber
599         my $query = "
600             SELECT items.biblionumber, items.biblioitemnumber, itemtypes.notforloan, items.notforloan AS itemnotforloan
601             FROM   items
602             LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
603             LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
604             WHERE  itemnumber=$qitem
605         ";
606         $sth = $dbh->prepare($query);
607     }
608     else {
609         my $qbc = $dbh->quote($barcode);
610         # Look up the item by barcode
611         my $query = "
612             SELECT items.biblionumber, items.biblioitemnumber, itemtypes.notforloan, items.notforloan AS itemnotforloan
613             FROM   items
614             LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
615             LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
616             WHERE  items.biblioitemnumber = biblioitems.biblioitemnumber
617               AND biblioitems.itemtype = itemtypes.itemtype
618               AND barcode=$qbc
619         ";
620         $sth = $dbh->prepare($query);
621
622         # FIXME - This function uses $item later on. Ought to set it here.
623     }
624     $sth->execute;
625     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item ) = $sth->fetchrow_array;
626     $sth->finish;
627     # if item is not for loan it cannot be reserved either.....
628     #    execption to notforloan is where items.notforloan < 0 :  This indicates the item is holdable. 
629     return ( 0, 0 ) if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
630
631     # get the reserves...
632     # Find this item in the reserves
633     my @reserves = _Findgroupreserve( $bibitem, $biblio, $item );
634     my $count    = scalar @reserves;
635
636     # $priority and $highest are used to find the most important item
637     # in the list returned by &_Findgroupreserve. (The lower $priority,
638     # the more important the item.)
639     # $highest is the most important item we've seen so far.
640     my $priority = 10000000;
641     my $highest;
642     if ($count) {
643         foreach my $res (@reserves) {
644             # FIXME - $item might be undefined or empty: the caller
645             # might be searching by barcode.
646             if ( $res->{'itemnumber'} == $item && $res->{'priority'} == 0) {
647                 # Found it
648                 return ( "Waiting", $res );
649             }
650             else {
651                 # See if this item is more important than what we've got
652                 # so far.
653                 if ( $res->{'priority'} != 0 && $res->{'priority'} < $priority )
654                 {
655                     $priority = $res->{'priority'};
656                     $highest  = $res;
657                 }
658             }
659         }
660     }
661
662     # If we get this far, then no exact match was found. Print the
663     # most important item on the list. I think this tells us who's
664     # next in line to get this book.
665     if ($highest) {    # FIXME - $highest might be undefined
666         $highest->{'itemnumber'} = $item;
667         return ( "Reserved", $highest );
668     }
669     else {
670         return ( 0, 0 );
671     }
672 }
673
674 =item CancelReserve
675
676   &CancelReserve($biblionumber, $itemnumber, $borrowernumber);
677
678 Cancels a reserve.
679
680 Use either C<$biblionumber> or C<$itemnumber> to specify the item to
681 cancel, but not both: if both are given, C<&CancelReserve> does
682 nothing.
683
684 C<$borrowernumber> is the borrower number of the patron on whose
685 behalf the book was reserved.
686
687 If C<$biblionumber> was given, C<&CancelReserve> also adjusts the
688 priorities of the other people who are waiting on the book.
689
690 =cut
691
692 sub CancelReserve {
693     my ( $biblio, $item, $borr ) = @_;
694     my $dbh = C4::Context->dbh;
695         if ( $item and $borr ) {
696         # removing a waiting reserve record....
697         # update the database...
698         my $query = "
699             UPDATE reserves
700             SET    cancellationdate = now(),
701                    found            = Null,
702                    priority         = 0
703             WHERE  itemnumber       = ?
704              AND   borrowernumber   = ?
705         ";
706         my $sth = $dbh->prepare($query);
707         $sth->execute( $item, $borr );
708         $sth->finish;
709         $query = "
710             INSERT INTO old_reserves
711             SELECT * FROM reserves
712             WHERE  itemnumber       = ?
713              AND   borrowernumber   = ?
714         ";
715         $sth = $dbh->prepare($query);
716         $sth->execute( $item, $borr );
717         $query = "
718             DELETE FROM reserves
719             WHERE  itemnumber       = ?
720              AND   borrowernumber   = ?
721         ";
722         $sth = $dbh->prepare($query);
723         $sth->execute( $item, $borr );
724     }
725     else {
726         # removing a reserve record....
727         # get the prioritiy on this record....
728         my $priority;
729         my $query = qq/
730             SELECT priority FROM reserves
731             WHERE biblionumber   = ?
732               AND borrowernumber = ?
733               AND cancellationdate IS NULL
734               AND itemnumber IS NULL
735         /;
736         my $sth = $dbh->prepare($query);
737         $sth->execute( $biblio, $borr );
738         ($priority) = $sth->fetchrow_array;
739         $sth->finish;
740         $query = qq/
741             UPDATE reserves
742             SET    cancellationdate = now(),
743                    found            = Null,
744                    priority         = 0
745             WHERE  biblionumber     = ?
746               AND  borrowernumber   = ?
747         /;
748
749         # update the database, removing the record...
750         $sth = $dbh->prepare($query);
751         $sth->execute( $biblio, $borr );
752         $sth->finish;
753
754         $query = qq/
755             INSERT INTO old_reserves
756             SELECT * FROM reserves
757             WHERE  biblionumber     = ?
758               AND  borrowernumber   = ?
759         /;
760         $sth = $dbh->prepare($query);
761         $sth->execute( $biblio, $borr );
762
763         $query = qq/
764             DELETE FROM reserves
765             WHERE  biblionumber     = ?
766               AND  borrowernumber   = ?
767         /;
768         $sth = $dbh->prepare($query);
769         $sth->execute( $biblio, $borr );
770
771         # now fix the priority on the others....
772         _FixPriority( $priority, $biblio );
773     }
774 }
775
776 =item ModReserve
777
778 =over 4
779
780 ModReserve($rank, $biblio, $borrower, $branch[, $itemnumber])
781
782 =back
783
784 Change a hold request's priority or cancel it.
785
786 C<$rank> specifies the effect of the change.  If C<$rank>
787 is 'W' or 'n', nothing happens.  This corresponds to leaving a
788 request alone when changing its priority in the holds queue
789 for a bib.
790
791 If C<$rank> is 'del', the hold request is cancelled.
792
793 If C<$rank> is an integer greater than zero, the priority of
794 the request is set to that value.  Since priority != 0 means
795 that the item is not waiting on the hold shelf, setting the 
796 priority to a non-zero value also sets the request's found
797 status and waiting date to NULL. 
798
799 The optional C<$itemnumber> parameter is used only when
800 C<$rank> is a non-zero integer; if supplied, the itemnumber 
801 of the hold request is set accordingly; if omitted, the itemnumber
802 is cleared.
803
804 FIXME: Note that the forgoing can have the effect of causing
805 item-level hold requests to turn into title-level requests.  This
806 will be fixed once reserves has separate columns for requested
807 itemnumber and supplying itemnumber.
808
809 =cut
810
811 sub ModReserve {
812     #subroutine to update a reserve
813     my ( $rank, $biblio, $borrower, $branch , $itemnumber) = @_;
814      return if $rank eq "W";
815      return if $rank eq "n";
816     my $dbh = C4::Context->dbh;
817     if ( $rank eq "del" ) {
818         my $query = qq/
819             UPDATE reserves
820             SET    cancellationdate=now()
821             WHERE  biblionumber   = ?
822              AND   borrowernumber = ?
823         /;
824         my $sth = $dbh->prepare($query);
825         $sth->execute( $biblio, $borrower );
826         $sth->finish;
827         $query = qq/
828             INSERT INTO old_reserves
829             SELECT *
830             FROM   reserves 
831             WHERE  biblionumber   = ?
832              AND   borrowernumber = ?
833         /;
834         $sth = $dbh->prepare($query);
835         $sth->execute( $biblio, $borrower );
836         $query = qq/
837             DELETE FROM reserves 
838             WHERE  biblionumber   = ?
839              AND   borrowernumber = ?
840         /;
841         $sth = $dbh->prepare($query);
842         $sth->execute( $biblio, $borrower );
843         
844     }
845     elsif ($rank =~ /^\d+/ and $rank > 0) {
846         my $query = qq/
847         UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
848             WHERE biblionumber   = ?
849              AND borrowernumber = ?
850         /;
851         my $sth = $dbh->prepare($query);
852         $sth->execute( $rank, $branch,$itemnumber, $biblio, $borrower);
853         $sth->finish;
854         _FixPriority( $biblio, $borrower, $rank);
855     }
856 }
857
858 =item ModReserveFill
859
860   &ModReserveFill($reserve);
861
862 Fill a reserve. If I understand this correctly, this means that the
863 reserved book has been found and given to the patron who reserved it.
864
865 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
866 whose keys are fields from the reserves table in the Koha database.
867
868 =cut
869
870 sub ModReserveFill {
871     my ($res) = @_;
872     my $dbh = C4::Context->dbh;
873     # fill in a reserve record....
874     my $biblionumber = $res->{'biblionumber'};
875     my $borrowernumber    = $res->{'borrowernumber'};
876     my $resdate = $res->{'reservedate'};
877
878     # get the priority on this record....
879     my $priority;
880     my $query = "SELECT priority
881                  FROM   reserves
882                  WHERE  biblionumber   = ?
883                   AND   borrowernumber = ?
884                   AND   reservedate    = ?";
885     my $sth = $dbh->prepare($query);
886     $sth->execute( $biblionumber, $borrowernumber, $resdate );
887     ($priority) = $sth->fetchrow_array;
888     $sth->finish;
889
890     # update the database...
891     $query = "UPDATE reserves
892                   SET    found            = 'F',
893                          priority         = 0
894                  WHERE  biblionumber     = ?
895                     AND reservedate      = ?
896                     AND borrowernumber   = ?
897                 ";
898     $sth = $dbh->prepare($query);
899     $sth->execute( $biblionumber, $resdate, $borrowernumber );
900     $sth->finish;
901
902     # move to old_reserves
903     $query = "INSERT INTO old_reserves
904                  SELECT * FROM reserves
905                  WHERE  biblionumber     = ?
906                     AND reservedate      = ?
907                     AND borrowernumber   = ?
908                 ";
909     $sth = $dbh->prepare($query);
910     $sth->execute( $biblionumber, $resdate, $borrowernumber );
911     $query = "DELETE FROM reserves
912                  WHERE  biblionumber     = ?
913                     AND reservedate      = ?
914                     AND borrowernumber   = ?
915                 ";
916     $sth = $dbh->prepare($query);
917     $sth->execute( $biblionumber, $resdate, $borrowernumber );
918     
919     # now fix the priority on the others (if the priority wasn't
920     # already sorted!)....
921     unless ( $priority == 0 ) {
922         _FixPriority( $priority, $biblionumber );
923     }
924 }
925
926 =item ModReserveStatus
927
928 &ModReserveStatus($itemnumber, $newstatus);
929
930 Update the reserve status for the active (priority=0) reserve.
931
932 $itemnumber is the itemnumber the reserve is on
933
934 $newstatus is the new status.
935
936 =cut
937
938 sub ModReserveStatus {
939
940     #first : check if we have a reservation for this item .
941     my ($itemnumber, $newstatus) = @_;
942     my $dbh          = C4::Context->dbh;
943     my $query = " UPDATE reserves
944     SET    found=?,waitingdate = now()
945     WHERE itemnumber=?
946       AND found IS NULL
947       AND priority = 0
948     ";
949     my $sth_set = $dbh->prepare($query);
950     $sth_set->execute( $newstatus, $itemnumber );
951 }
952
953 =item ModReserveAffect
954
955 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
956
957 This function affect an item and a status for a given reserve
958 The itemnumber parameter is used to find the biblionumber.
959 with the biblionumber & the borrowernumber, we can affect the itemnumber
960 to the correct reserve.
961
962 if $transferToDo is not set, then the status is set to "Waiting" as well.
963 otherwise, a transfer is on the way, and the end of the transfer will 
964 take care of the waiting status
965 =cut
966
967 sub ModReserveAffect {
968     my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
969     my $dbh = C4::Context->dbh;
970
971     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
972     # attached to $itemnumber
973     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
974     $sth->execute($itemnumber);
975     my ($biblionumber) = $sth->fetchrow;
976     # If we affect a reserve that has to be transfered, don't set to Waiting
977     my $query;
978     if ($transferToDo) {
979     $query = "
980         UPDATE reserves
981         SET    priority = 0,
982                itemnumber = ?
983         WHERE borrowernumber = ?
984           AND biblionumber = ?
985     ";
986     }
987     else {
988     # affect the reserve to Waiting as well.
989     $query = "
990         UPDATE reserves
991         SET     priority = 0,
992                 found = 'W',
993                 waitingdate=now(),
994                 itemnumber = ?
995         WHERE borrowernumber = ?
996           AND biblionumber = ?
997     ";
998     }
999     $sth = $dbh->prepare($query);
1000     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1001     _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo );
1002
1003     return;
1004 }
1005
1006 =item ModReserveCancelAll
1007
1008 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1009
1010     function to cancel reserv,check other reserves, and transfer document if it's necessary
1011
1012 =cut
1013
1014 sub ModReserveCancelAll {
1015     my $messages;
1016     my $nextreservinfo;
1017     my ( $itemnumber, $borrowernumber ) = @_;
1018
1019     #step 1 : cancel the reservation
1020     my $CancelReserve = CancelReserve( undef, $itemnumber, $borrowernumber );
1021
1022     #step 2 launch the subroutine of the others reserves
1023     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1024
1025     return ( $messages, $nextreservinfo );
1026 }
1027
1028 =item ModReserveMinusPriority
1029
1030 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1031
1032 Reduce the values of queuded list     
1033
1034 =cut
1035
1036 sub ModReserveMinusPriority {
1037     my ( $itemnumber, $borrowernumber, $biblionumber ) = @_;
1038
1039     #first step update the value of the first person on reserv
1040     my $dbh   = C4::Context->dbh;
1041     my $query = "
1042         UPDATE reserves
1043         SET    priority = 0 , itemnumber = ? 
1044         WHERE  borrowernumber=?
1045           AND  biblionumber=?
1046     ";
1047     my $sth_upd = $dbh->prepare($query);
1048     $sth_upd->execute( $itemnumber, $borrowernumber, $biblionumber );
1049     # second step update all others reservs
1050     _FixPriority($biblionumber, $borrowernumber, '0');
1051 }
1052
1053 =item GetReserveInfo
1054
1055 &GetReserveInfo($borrowernumber,$biblionumber);
1056
1057  Get item and borrower details for a current hold.
1058  Current implementation this query should have a single result.
1059 =cut
1060
1061 sub GetReserveInfo {
1062         my ( $borrowernumber, $biblionumber ) = @_;
1063     my $dbh = C4::Context->dbh;
1064         my $strsth="SELECT reservedate, reservenotes, reserves.borrowernumber,
1065                                 reserves.biblionumber, reserves.branchcode,
1066                                 notificationdate, reminderdate, priority, found,
1067                                 firstname, surname, phone, 
1068                                 email, address, address2,
1069                                 cardnumber, city, zipcode,
1070                                 biblio.title, biblio.author,
1071                                 items.holdingbranch, items.itemcallnumber, items.itemnumber, 
1072                                 barcode, notes
1073                         FROM reserves left join items 
1074                                 ON items.itemnumber=reserves.itemnumber , 
1075                                 borrowers, biblio 
1076                         WHERE 
1077                                 reserves.borrowernumber=?  &&
1078                                 reserves.biblionumber=? && 
1079                                 reserves.borrowernumber=borrowers.borrowernumber && 
1080                                 reserves.biblionumber=biblio.biblionumber ";
1081         my $sth = $dbh->prepare($strsth); 
1082         $sth->execute($borrowernumber,$biblionumber);
1083
1084         my $data = $sth->fetchrow_hashref;
1085         return $data;
1086
1087 }
1088
1089 =item IsAvailableForItemLevelRequest
1090
1091 =over 4
1092
1093 my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1094
1095 =back
1096
1097 Checks whether a given item record is available for an
1098 item-level hold request.  An item is available if
1099
1100 * it is not lost AND 
1101 * it is not damaged AND 
1102 * it is not withdrawn AND 
1103 * does not have a not for loan value > 0
1104
1105 Whether or not the item is currently on loan is 
1106 also checked - if the AllowOnShelfHolds system preference
1107 is ON, an item can be requested even if it is currently
1108 on loan to somebody else.  If the system preference
1109 is OFF, an item that is currently checked out cannot
1110 be the target of an item-level hold request.
1111
1112 Note that IsAvailableForItemLevelRequest() does not
1113 check if the staff operator is authorized to place
1114 a request on the item - in particular,
1115 this routine does not check IndependantBranches
1116 and canreservefromotherbranches.
1117
1118 =cut
1119
1120 sub IsAvailableForItemLevelRequest {
1121     my $itemnumber = shift;
1122    
1123     my $item = GetItem($itemnumber);
1124
1125     # must check the notforloan setting of the itemtype
1126     # FIXME - a lot of places in the code do this
1127     #         or something similar - need to be
1128     #         consolidated
1129     my $dbh = C4::Context->dbh;
1130     my $notforloan_query;
1131     if (C4::Context->preference('item-level_itypes')) {
1132         $notforloan_query = "SELECT itemtypes.notforloan
1133                              FROM items
1134                              JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1135                              WHERE itemnumber = ?";
1136     } else {
1137         $notforloan_query = "SELECT itemtypes.notforloan
1138                              FROM items
1139                              JOIN biblioitems USING (biblioitemnumber)
1140                              JOIN itemtypes USING (itemtype)
1141                              WHERE itemnumber = ?";
1142     }
1143     my $sth = $dbh->prepare($notforloan_query);
1144     $sth->execute($itemnumber);
1145     my $notforloan_per_itemtype = 0;
1146     if (my ($notforloan) = $sth->fetchrow_array) {
1147         $notforloan_per_itemtype = 1 if $notforloan;
1148     }
1149
1150     my $available_per_item = 1;
1151     $available_per_item = 0 if $item->{itemlost} or
1152                                ( $item->{notforloan} > 0 ) or
1153                                ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1154                                $item->{wthdrawn} or
1155                                $notforloan_per_itemtype;
1156
1157
1158     if (C4::Context->preference('AllowOnShelfHolds')) {
1159         return $available_per_item;
1160     } else {
1161         return ($available_per_item and $item->{onloan}); 
1162     }
1163 }
1164
1165 =item _FixPriority
1166
1167 &_FixPriority($biblio,$borrowernumber,$rank);
1168
1169  Only used internally (so don't export it)
1170  Changed how this functions works #
1171  Now just gets an array of reserves in the rank order and updates them with
1172  the array index (+1 as array starts from 0)
1173  and if $rank is supplied will splice item from the array and splice it back in again
1174  in new priority rank
1175
1176 =cut 
1177
1178 sub _FixPriority {
1179     my ( $biblio, $borrowernumber, $rank ) = @_;
1180     my $dbh = C4::Context->dbh;
1181      if ( $rank eq "del" ) {
1182          CancelReserve( $biblio, undef, $borrowernumber );
1183      }
1184     if ( $rank eq "W" || $rank eq "0" ) {
1185
1186         # make sure priority for waiting items is 0
1187         my $query = qq/
1188             UPDATE reserves
1189             SET    priority = 0
1190             WHERE biblionumber = ?
1191               AND borrowernumber = ?
1192               AND found ='W'
1193         /;
1194         my $sth = $dbh->prepare($query);
1195         $sth->execute( $biblio, $borrowernumber );
1196     }
1197     my @priority;
1198     my @reservedates;
1199
1200     # get whats left
1201 # FIXME adding a new security in returned elements for changing priority,
1202 # now, we don't care anymore any reservations with itemnumber linked (suppose a waiting reserve)
1203         # This is wrong a waiting reserve has W set
1204         # The assumption that having an itemnumber set means waiting is wrong and should be corrected any place it occurs
1205     my $query = qq/
1206         SELECT borrowernumber, reservedate, constrainttype
1207         FROM   reserves
1208         WHERE  biblionumber   = ?
1209           AND  ((found <> 'W') or found is NULL)
1210         ORDER BY priority ASC
1211     /;
1212     my $sth = $dbh->prepare($query);
1213     $sth->execute($biblio);
1214     while ( my $line = $sth->fetchrow_hashref ) {
1215         push( @reservedates, $line );
1216         push( @priority,     $line );
1217     }
1218
1219     # To find the matching index
1220     my $i;
1221     my $key = -1;    # to allow for 0 to be a valid result
1222     for ( $i = 0 ; $i < @priority ; $i++ ) {
1223         if ( $borrowernumber == $priority[$i]->{'borrowernumber'} ) {
1224             $key = $i;    # save the index
1225             last;
1226         }
1227     }
1228
1229     # if index exists in array then move it to new position
1230     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1231         my $new_rank = $rank -
1232           1;    # $new_rank is what you want the new index to be in the array
1233         my $moving_item = splice( @priority, $key, 1 );
1234         splice( @priority, $new_rank, 0, $moving_item );
1235     }
1236
1237     # now fix the priority on those that are left....
1238     $query = "
1239             UPDATE reserves
1240             SET    priority = ?
1241                 WHERE  biblionumber = ?
1242                  AND borrowernumber   = ?
1243                  AND reservedate = ?
1244          AND found IS NULL
1245     ";
1246     $sth = $dbh->prepare($query);
1247     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1248         $sth->execute(
1249             $j + 1, $biblio,
1250             $priority[$j]->{'borrowernumber'},
1251             $priority[$j]->{'reservedate'}
1252         );
1253         $sth->finish;
1254     }
1255 }
1256
1257 =item _Findgroupreserve
1258
1259   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1260
1261 ****** FIXME ******
1262 I don't know what this does, because I don't understand how reserve
1263 constraints work. I think the idea is that you reserve a particular
1264 biblio, and the constraint allows you to restrict it to a given
1265 biblioitem (e.g., if you want to borrow the audio book edition of "The
1266 Prophet", rather than the first available publication).
1267
1268 C<&_Findgroupreserve> returns :
1269 C<@results> is an array of references-to-hash whose keys are mostly
1270 fields from the reserves table of the Koha database, plus
1271 C<biblioitemnumber>.
1272
1273 =cut
1274
1275 sub _Findgroupreserve {
1276     my ( $bibitem, $biblio, $itemnumber ) = @_;
1277     my $dbh   = C4::Context->dbh;
1278
1279     # check for exact targetted match
1280     my $item_level_target_query = qq/
1281         SELECT reserves.biblionumber AS biblionumber,
1282                reserves.borrowernumber AS borrowernumber,
1283                reserves.reservedate AS reservedate,
1284                reserves.branchcode AS branchcode,
1285                reserves.cancellationdate AS cancellationdate,
1286                reserves.found AS found,
1287                reserves.reservenotes AS reservenotes,
1288                reserves.priority AS priority,
1289                reserves.timestamp AS timestamp,
1290                biblioitems.biblioitemnumber AS biblioitemnumber,
1291                reserves.itemnumber AS itemnumber
1292         FROM reserves
1293         JOIN biblioitems USING (biblionumber)
1294         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1295         WHERE found IS NULL
1296         AND priority > 0
1297         AND item_level_request = 1
1298         AND itemnumber = ?
1299     /;
1300     my $sth = $dbh->prepare($item_level_target_query);
1301     $sth->execute($itemnumber);
1302     my @results;
1303     if ( my $data = $sth->fetchrow_hashref ) {
1304         push( @results, $data );
1305     }
1306     return @results if @results;
1307     
1308     # check for title-level targetted match
1309     my $title_level_target_query = qq/
1310         SELECT reserves.biblionumber AS biblionumber,
1311                reserves.borrowernumber AS borrowernumber,
1312                reserves.reservedate AS reservedate,
1313                reserves.branchcode AS branchcode,
1314                reserves.cancellationdate AS cancellationdate,
1315                reserves.found AS found,
1316                reserves.reservenotes AS reservenotes,
1317                reserves.priority AS priority,
1318                reserves.timestamp AS timestamp,
1319                biblioitems.biblioitemnumber AS biblioitemnumber,
1320                reserves.itemnumber AS itemnumber
1321         FROM reserves
1322         JOIN biblioitems USING (biblionumber)
1323         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1324         WHERE found IS NULL
1325         AND priority > 0
1326         AND item_level_request = 0
1327         AND hold_fill_targets.itemnumber = ?
1328     /;
1329     $sth = $dbh->prepare($title_level_target_query);
1330     $sth->execute($itemnumber);
1331     @results = ();
1332     if ( my $data = $sth->fetchrow_hashref ) {
1333         push( @results, $data );
1334     }
1335     return @results if @results;
1336
1337     my $query = qq/
1338         SELECT reserves.biblionumber AS biblionumber,
1339                reserves.borrowernumber AS borrowernumber,
1340                reserves.reservedate AS reservedate,
1341                reserves.branchcode AS branchcode,
1342                reserves.cancellationdate AS cancellationdate,
1343                reserves.found AS found,
1344                reserves.reservenotes AS reservenotes,
1345                reserves.priority AS priority,
1346                reserves.timestamp AS timestamp,
1347                reserveconstraints.biblioitemnumber AS biblioitemnumber,
1348                reserves.itemnumber AS itemnumber
1349         FROM reserves
1350           LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1351         WHERE reserves.biblionumber = ?
1352           AND ( ( reserveconstraints.biblioitemnumber = ?
1353           AND reserves.borrowernumber = reserveconstraints.borrowernumber
1354           AND reserves.reservedate    =reserveconstraints.reservedate )
1355           OR  reserves.constrainttype='a' )
1356           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1357     /;
1358     $sth = $dbh->prepare($query);
1359     $sth->execute( $biblio, $bibitem, $itemnumber );
1360     @results = ();
1361     while ( my $data = $sth->fetchrow_hashref ) {
1362         push( @results, $data );
1363     }
1364     return @results;
1365 }
1366
1367 =item _koha_notify_reserve
1368
1369 =over 4
1370
1371 _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1372
1373 =back
1374
1375 Sends a notification to the patron that their hold has been filled (through
1376 ModReserveAffect, _not_ ModReserveFill)
1377
1378 =cut
1379
1380 sub _koha_notify_reserve {
1381     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1382
1383     my $dbh = C4::Context->dbh;
1384     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold Filled' } );
1385
1386     return if ( !defined( $messagingprefs->{'letter_code'} ) );
1387
1388     my $sth = $dbh->prepare("
1389         SELECT *
1390         FROM   reserves
1391         WHERE  borrowernumber = ?
1392             AND biblionumber = ?
1393     ");
1394     $sth->execute( $borrowernumber, $biblionumber );
1395     my $reserve = $sth->fetchrow_hashref;
1396     my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1397
1398     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1399
1400     my $letter = getletter( 'reserves', $messagingprefs->{'letter_code'} );
1401
1402     C4::Letters::parseletter( $letter, 'branches', $reserve->{'branchcode'} );
1403     C4::Letters::parseletter( $letter, 'borrowers', $reserve->{'borrowernumber'} );
1404     C4::Letters::parseletter( $letter, 'biblio', $reserve->{'biblionumber'} );
1405     C4::Letters::parseletter( $letter, 'reserves', $reserve->{'borrowernumber'}, $reserve->{'biblionumber'} );
1406
1407     if ( $reserve->{'itemnumber'} ) {
1408         C4::Letters::parseletter( $letter, 'items', $reserve->{'itemnumber'} );
1409     }
1410     $letter->{'content'} =~ s/<<[a-z0-9_]+\.[a-z0-9]+>>//g; #remove any stragglers
1411
1412     if ( -1 !=  firstidx { $_ eq 'email' } @{$messagingprefs->{transports}} ) {
1413         # aka, 'email' in ->{'transports'}
1414         C4::Letters::EnqueueLetter(
1415             {   letter                 => $letter,
1416                 borrowernumber         => $borrowernumber,
1417                 message_transport_type => 'email',
1418                 from_address           => $admin_email_address,
1419             }
1420         );
1421     }
1422
1423     if ( -1 != firstidx { $_ eq 'sms' } @{$messagingprefs->{transports}} ) {
1424         C4::Letters::EnqueueLetter(
1425             {   letter                 => $letter,
1426                 borrowernumber         => $borrowernumber,
1427                 message_transport_type => 'sms',
1428             }
1429         );
1430     }
1431 }
1432
1433 =back
1434
1435 =head1 AUTHOR
1436
1437 Koha Developement team <info@koha.org>
1438
1439 =cut
1440
1441 1;