Merge branch 'bug_8557' into 3.12-master
[koha.git] / C4 / Members.pm
1 package C4::Members;
2
3 # Copyright 2000-2003 Katipo Communications
4 # Copyright 2010 BibLibre
5 # Parts Copyright 2010 Catalyst IT
6 #
7 # This file is part of Koha.
8 #
9 # Koha is free software; you can redistribute it and/or modify it under the
10 # terms of the GNU General Public License as published by the Free Software
11 # Foundation; either version 2 of the License, or (at your option) any later
12 # version.
13 #
14 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License along
19 # with Koha; if not, write to the Free Software Foundation, Inc.,
20 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22
23 use strict;
24 #use warnings; FIXME - Bug 2505
25 use C4::Context;
26 use C4::Dates qw(format_date_in_iso format_date);
27 use Digest::MD5 qw(md5_base64);
28 use String::Random qw( random_string );
29 use Date::Calc qw/Today Add_Delta_YM check_date Date_to_Days/;
30 use C4::Log; # logaction
31 use C4::Overdues;
32 use C4::Reserves;
33 use C4::Accounts;
34 use C4::Biblio;
35 use C4::Letters;
36 use C4::SQLHelper qw(InsertInTable UpdateInTable SearchInTable);
37 use C4::Members::Attributes qw(SearchIdMatchingAttribute);
38 use C4::NewsChannels; #get slip news
39 use DateTime;
40 use DateTime::Format::DateParse;
41 use Koha::DateUtils;
42 use Text::Unaccent qw( unac_string );
43
44 our ($VERSION,@ISA,@EXPORT,@EXPORT_OK,$debug);
45
46 BEGIN {
47     $VERSION = 3.07.00.049;
48     $debug = $ENV{DEBUG} || 0;
49     require Exporter;
50     @ISA = qw(Exporter);
51     #Get data
52     push @EXPORT, qw(
53         &Search
54         &GetMemberDetails
55         &GetMemberRelatives
56         &GetMember
57
58         &GetGuarantees
59
60         &GetMemberIssuesAndFines
61         &GetPendingIssues
62         &GetAllIssues
63
64         &get_institutions
65         &getzipnamecity
66         &getidcity
67
68         &GetFirstValidEmailAddress
69
70         &GetAge
71         &GetCities
72         &GetRoadTypes
73         &GetRoadTypeDetails
74         &GetSortDetails
75         &GetTitles
76
77         &GetPatronImage
78         &PutPatronImage
79         &RmPatronImage
80
81         &GetHideLostItemsPreference
82
83         &IsMemberBlocked
84         &GetMemberAccountRecords
85         &GetBorNotifyAcctRecord
86
87         &GetborCatFromCatType
88         &GetBorrowercategory
89         GetBorrowerCategorycode
90         &GetBorrowercategoryList
91
92         &GetBorrowersToExpunge
93         &GetBorrowersWhoHaveNeverBorrowed
94         &GetBorrowersWithIssuesHistoryOlderThan
95
96         &GetExpiryDate
97
98         &AddMessage
99         &DeleteMessage
100         &GetMessages
101         &GetMessagesCount
102
103         &IssueSlip
104         GetBorrowersWithEmail
105     );
106
107     #Modify data
108     push @EXPORT, qw(
109         &ModMember
110         &changepassword
111          &ModPrivacy
112     );
113
114     #Delete data
115     push @EXPORT, qw(
116         &DelMember
117     );
118
119     #Insert data
120     push @EXPORT, qw(
121         &AddMember
122         &AddMember_Opac
123         &add_member_orgs
124         &MoveMemberToDeleted
125         &ExtendMemberSubscriptionTo
126     );
127
128     #Check data
129     push @EXPORT, qw(
130         &checkuniquemember
131         &checkuserpassword
132         &Check_Userid
133         &Generate_Userid
134         &fixEthnicity
135         &ethnicitycategories
136         &fixup_cardnumber
137         &checkcardnumber
138     );
139 }
140
141 =head1 NAME
142
143 C4::Members - Perl Module containing convenience functions for member handling
144
145 =head1 SYNOPSIS
146
147 use C4::Members;
148
149 =head1 DESCRIPTION
150
151 This module contains routines for adding, modifying and deleting members/patrons/borrowers 
152
153 =head1 FUNCTIONS
154
155 =head2 Search
156
157   $borrowers_result_array_ref = &Search($filter,$orderby, $limit, 
158                        $columns_out, $search_on_fields,$searchtype);
159
160 Looks up patrons (borrowers) on filter. A wrapper for SearchInTable('borrowers').
161
162 For C<$filter>, C<$orderby>, C<$limit>, C<&columns_out>, C<&search_on_fields> and C<&searchtype>
163 refer to C4::SQLHelper:SearchInTable().
164
165 Special C<$filter> key '' is effectively expanded to search on surname firstname othernamescw
166 and cardnumber unless C<&search_on_fields> is defined
167
168 Examples:
169
170   $borrowers = Search('abcd', 'cardnumber');
171
172   $borrowers = Search({''=>'abcd', category_type=>'I'}, 'surname');
173
174 =cut
175
176 sub _express_member_find {
177     my ($filter) = @_;
178
179     # this is used by circulation everytime a new borrowers cardnumber is scanned
180     # so we can check an exact match first, if that works return, otherwise do the rest
181     my $dbh   = C4::Context->dbh;
182     my $query = "SELECT borrowernumber FROM borrowers WHERE cardnumber = ?";
183     if ( my $borrowernumber = $dbh->selectrow_array($query, undef, $filter) ) {
184         return( {"borrowernumber"=>$borrowernumber} );
185     }
186
187     my ($search_on_fields, $searchtype);
188     if ( length($filter) == 1 ) {
189         $search_on_fields = [ qw(surname) ];
190         $searchtype = 'start_with';
191     } else {
192         $search_on_fields = [ qw(surname firstname othernames cardnumber) ];
193         $searchtype = 'contain';
194     }
195
196     return (undef, $search_on_fields, $searchtype);
197 }
198
199 sub Search {
200     my ( $filter, $orderby, $limit, $columns_out, $search_on_fields, $searchtype ) = @_;
201
202     my $search_string;
203     my $found_borrower;
204
205     if ( my $fr = ref $filter ) {
206         if ( $fr eq "HASH" ) {
207             if ( my $search_string = $filter->{''} ) {
208                 my ($member_filter, $member_search_on_fields, $member_searchtype) = _express_member_find($search_string);
209                 if ($member_filter) {
210                     $filter = $member_filter;
211                     $found_borrower = 1;
212                 } else {
213                     $search_on_fields ||= $member_search_on_fields;
214                     $searchtype ||= $member_searchtype;
215                 }
216             }
217         }
218         else {
219             $search_string = $filter;
220         }
221     }
222     else {
223         $search_string = $filter;
224         my ($member_filter, $member_search_on_fields, $member_searchtype) = _express_member_find($search_string);
225         if ($member_filter) {
226             $filter = $member_filter;
227             $found_borrower = 1;
228         } else {
229             $search_on_fields ||= $member_search_on_fields;
230             $searchtype ||= $member_searchtype;
231         }
232     }
233
234     if ( !$found_borrower && C4::Context->preference('ExtendedPatronAttributes') && $search_string ) {
235         my $matching_records = C4::Members::Attributes::SearchIdMatchingAttribute($search_string);
236         if(scalar(@$matching_records)>0) {
237             if ( my $fr = ref $filter ) {
238                 if ( $fr eq "HASH" ) {
239                     my %f = %$filter;
240                     $filter = [ $filter ];
241                     delete $f{''};
242                     push @$filter, { %f, "borrowernumber"=>$$matching_records };
243                 }
244                 else {
245                     push @$filter, {"borrowernumber"=>$matching_records};
246                 }
247             }
248             else {
249                 $filter = [ $filter ];
250                 push @$filter, {"borrowernumber"=>$matching_records};
251             }
252                 }
253     }
254
255     # $showallbranches was not used at the time SearchMember() was mainstreamed into Search().
256     # Mentioning for the reference
257
258     if ( C4::Context->preference("IndependantBranches") ) { # && !$showallbranches){
259         if ( my $userenv = C4::Context->userenv ) {
260             my $branch =  $userenv->{'branch'};
261             if ( ($userenv->{flags} % 2 !=1) &&
262                  $branch && $branch ne "insecure" ){
263
264                 if (my $fr = ref $filter) {
265                     if ( $fr eq "HASH" ) {
266                         $filter->{branchcode} = $branch;
267                     }
268                     else {
269                         foreach (@$filter) {
270                             $_ = { '' => $_ } unless ref $_;
271                             $_->{branchcode} = $branch;
272                         }
273                     }
274                 }
275                 else {
276                     $filter = { '' => $filter, branchcode => $branch };
277                 }
278             }      
279         }
280     }
281
282     if ($found_borrower) {
283         $searchtype = "exact";
284     }
285     $searchtype ||= "start_with";
286
287         return SearchInTable( "borrowers", $filter, $orderby, $limit, $columns_out, $search_on_fields, $searchtype );
288 }
289
290 =head2 GetMemberDetails
291
292 ($borrower) = &GetMemberDetails($borrowernumber, $cardnumber);
293
294 Looks up a patron and returns information about him or her. If
295 C<$borrowernumber> is true (nonzero), C<&GetMemberDetails> looks
296 up the borrower by number; otherwise, it looks up the borrower by card
297 number.
298
299 C<$borrower> is a reference-to-hash whose keys are the fields of the
300 borrowers table in the Koha database. In addition,
301 C<$borrower-E<gt>{flags}> is a hash giving more detailed information
302 about the patron. Its keys act as flags :
303
304     if $borrower->{flags}->{LOST} {
305         # Patron's card was reported lost
306     }
307
308 If the state of a flag means that the patron should not be
309 allowed to borrow any more books, then it will have a C<noissues> key
310 with a true value.
311
312 See patronflags for more details.
313
314 C<$borrower-E<gt>{authflags}> is a hash giving more detailed information
315 about the top-level permissions flags set for the borrower.  For example,
316 if a user has the "editcatalogue" permission,
317 C<$borrower-E<gt>{authflags}-E<gt>{editcatalogue}> will exist and have
318 the value "1".
319
320 =cut
321
322 sub GetMemberDetails {
323     my ( $borrowernumber, $cardnumber ) = @_;
324     my $dbh = C4::Context->dbh;
325     my $query;
326     my $sth;
327     if ($borrowernumber) {
328         $sth = $dbh->prepare("SELECT borrowers.*,category_type,categories.description,reservefee,enrolmentperiod FROM borrowers LEFT JOIN categories ON borrowers.categorycode=categories.categorycode WHERE  borrowernumber=?");
329         $sth->execute($borrowernumber);
330     }
331     elsif ($cardnumber) {
332         $sth = $dbh->prepare("SELECT borrowers.*,category_type,categories.description,reservefee,enrolmentperiod FROM borrowers LEFT JOIN categories ON borrowers.categorycode=categories.categorycode WHERE cardnumber=?");
333         $sth->execute($cardnumber);
334     }
335     else {
336         return;
337     }
338     my $borrower = $sth->fetchrow_hashref;
339     my ($amount) = GetMemberAccountRecords( $borrowernumber);
340     $borrower->{'amountoutstanding'} = $amount;
341     # FIXME - patronflags calls GetMemberAccountRecords... just have patronflags return $amount
342     my $flags = patronflags( $borrower);
343     my $accessflagshash;
344
345     $sth = $dbh->prepare("select bit,flag from userflags");
346     $sth->execute;
347     while ( my ( $bit, $flag ) = $sth->fetchrow ) {
348         if ( $borrower->{'flags'} && $borrower->{'flags'} & 2**$bit ) {
349             $accessflagshash->{$flag} = 1;
350         }
351     }
352     $borrower->{'flags'}     = $flags;
353     $borrower->{'authflags'} = $accessflagshash;
354
355     # For the purposes of making templates easier, we'll define a
356     # 'showname' which is the alternate form the user's first name if 
357     # 'other name' is defined.
358     if ($borrower->{category_type} eq 'I') {
359         $borrower->{'showname'} = $borrower->{'othernames'};
360         $borrower->{'showname'} .= " $borrower->{'firstname'}" if $borrower->{'firstname'};
361     } else {
362         $borrower->{'showname'} = $borrower->{'firstname'};
363     }
364
365     return ($borrower);    #, $flags, $accessflagshash);
366 }
367
368 =head2 patronflags
369
370  $flags = &patronflags($patron);
371
372 This function is not exported.
373
374 The following will be set where applicable:
375  $flags->{CHARGES}->{amount}        Amount of debt
376  $flags->{CHARGES}->{noissues}      Set if debt amount >$5.00 (or syspref noissuescharge)
377  $flags->{CHARGES}->{message}       Message -- deprecated
378
379  $flags->{CREDITS}->{amount}        Amount of credit
380  $flags->{CREDITS}->{message}       Message -- deprecated
381
382  $flags->{  GNA  }                  Patron has no valid address
383  $flags->{  GNA  }->{noissues}      Set for each GNA
384  $flags->{  GNA  }->{message}       "Borrower has no valid address" -- deprecated
385
386  $flags->{ LOST  }                  Patron's card reported lost
387  $flags->{ LOST  }->{noissues}      Set for each LOST
388  $flags->{ LOST  }->{message}       Message -- deprecated
389
390  $flags->{DBARRED}                  Set if patron debarred, no access
391  $flags->{DBARRED}->{noissues}      Set for each DBARRED
392  $flags->{DBARRED}->{message}       Message -- deprecated
393
394  $flags->{ NOTES }
395  $flags->{ NOTES }->{message}       The note itself.  NOT deprecated
396
397  $flags->{ ODUES }                  Set if patron has overdue books.
398  $flags->{ ODUES }->{message}       "Yes"  -- deprecated
399  $flags->{ ODUES }->{itemlist}      ref-to-array: list of overdue books
400  $flags->{ ODUES }->{itemlisttext}  Text list of overdue items -- deprecated
401
402  $flags->{WAITING}                  Set if any of patron's reserves are available
403  $flags->{WAITING}->{message}       Message -- deprecated
404  $flags->{WAITING}->{itemlist}      ref-to-array: list of available items
405
406 =over 
407
408 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
409 overdue items. Its elements are references-to-hash, each describing an
410 overdue item. The keys are selected fields from the issues, biblio,
411 biblioitems, and items tables of the Koha database.
412
413 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
414 the overdue items, one per line.  Deprecated.
415
416 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
417 available items. Each element is a reference-to-hash whose keys are
418 fields from the reserves table of the Koha database.
419
420 =back
421
422 All the "message" fields that include language generated in this function are deprecated, 
423 because such strings belong properly in the display layer.
424
425 The "message" field that comes from the DB is OK.
426
427 =cut
428
429 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
430 # FIXME rename this function.
431 sub patronflags {
432     my %flags;
433     my ( $patroninformation) = @_;
434     my $dbh=C4::Context->dbh;
435     my ($amount) = GetMemberAccountRecords( $patroninformation->{'borrowernumber'});
436     if ( $amount > 0 ) {
437         my %flaginfo;
438         my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
439         $flaginfo{'message'} = sprintf "Patron owes \$%.02f", $amount;
440         $flaginfo{'amount'}  = sprintf "%.02f", $amount;
441         if ( $amount > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
442             $flaginfo{'noissues'} = 1;
443         }
444         $flags{'CHARGES'} = \%flaginfo;
445     }
446     elsif ( $amount < 0 ) {
447         my %flaginfo;
448         $flaginfo{'message'} = sprintf "Patron has credit of \$%.02f", -$amount;
449         $flaginfo{'amount'}  = sprintf "%.02f", $amount;
450         $flags{'CREDITS'} = \%flaginfo;
451     }
452     if (   $patroninformation->{'gonenoaddress'}
453         && $patroninformation->{'gonenoaddress'} == 1 )
454     {
455         my %flaginfo;
456         $flaginfo{'message'}  = 'Borrower has no valid address.';
457         $flaginfo{'noissues'} = 1;
458         $flags{'GNA'}         = \%flaginfo;
459     }
460     if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
461         my %flaginfo;
462         $flaginfo{'message'}  = 'Borrower\'s card reported lost.';
463         $flaginfo{'noissues'} = 1;
464         $flags{'LOST'}        = \%flaginfo;
465     }
466     if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
467         if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
468             my %flaginfo;
469             $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
470             $flaginfo{'message'}         = $patroninformation->{'debarredcomment'};
471             $flaginfo{'noissues'}        = 1;
472             $flaginfo{'dateend'}         = $patroninformation->{'debarred'};
473             $flags{'DBARRED'}           = \%flaginfo;
474         }
475     }
476     if (   $patroninformation->{'borrowernotes'}
477         && $patroninformation->{'borrowernotes'} )
478     {
479         my %flaginfo;
480         $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
481         $flags{'NOTES'}      = \%flaginfo;
482     }
483     my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
484     if ( $odues && $odues > 0 ) {
485         my %flaginfo;
486         $flaginfo{'message'}  = "Yes";
487         $flaginfo{'itemlist'} = $itemsoverdue;
488         foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
489             @$itemsoverdue )
490         {
491             $flaginfo{'itemlisttext'} .=
492               "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n";  # newline is display layer
493         }
494         $flags{'ODUES'} = \%flaginfo;
495     }
496     my @itemswaiting = C4::Reserves::GetReservesFromBorrowernumber( $patroninformation->{'borrowernumber'},'W' );
497     my $nowaiting = scalar @itemswaiting;
498     if ( $nowaiting > 0 ) {
499         my %flaginfo;
500         $flaginfo{'message'}  = "Reserved items available";
501         $flaginfo{'itemlist'} = \@itemswaiting;
502         $flags{'WAITING'}     = \%flaginfo;
503     }
504     return ( \%flags );
505 }
506
507
508 =head2 GetMember
509
510   $borrower = &GetMember(%information);
511
512 Retrieve the first patron record meeting on criteria listed in the
513 C<%information> hash, which should contain one or more
514 pairs of borrowers column names and values, e.g.,
515
516    $borrower = GetMember(borrowernumber => id);
517
518 C<&GetBorrower> returns a reference-to-hash whose keys are the fields of
519 the C<borrowers> table in the Koha database.
520
521 FIXME: GetMember() is used throughout the code as a lookup
522 on a unique key such as the borrowernumber, but this meaning is not
523 enforced in the routine itself.
524
525 =cut
526
527 #'
528 sub GetMember {
529     my ( %information ) = @_;
530     if (exists $information{borrowernumber} && !defined $information{borrowernumber}) {
531         #passing mysql's kohaadmin?? Makes no sense as a query
532         return;
533     }
534     my $dbh = C4::Context->dbh;
535     my $select =
536     q{SELECT borrowers.*, categories.category_type, categories.description
537     FROM borrowers 
538     LEFT JOIN categories on borrowers.categorycode=categories.categorycode WHERE };
539     my $more_p = 0;
540     my @values = ();
541     for (keys %information ) {
542         if ($more_p) {
543             $select .= ' AND ';
544         }
545         else {
546             $more_p++;
547         }
548
549         if (defined $information{$_}) {
550             $select .= "$_ = ?";
551             push @values, $information{$_};
552         }
553         else {
554             $select .= "$_ IS NULL";
555         }
556     }
557     $debug && warn $select, " ",values %information;
558     my $sth = $dbh->prepare("$select");
559     $sth->execute(map{$information{$_}} keys %information);
560     my $data = $sth->fetchall_arrayref({});
561     #FIXME interface to this routine now allows generation of a result set
562     #so whole array should be returned but bowhere in the current code expects this
563     if (@{$data} ) {
564         return $data->[0];
565     }
566
567     return;
568 }
569
570 =head2 GetMemberRelatives
571
572  @borrowernumbers = GetMemberRelatives($borrowernumber);
573
574  C<GetMemberRelatives> returns a borrowersnumber's list of guarantor/guarantees of the member given in parameter
575
576 =cut 
577 sub GetMemberRelatives {
578     my $borrowernumber = shift;
579     my $dbh = C4::Context->dbh;
580     my @glist;
581
582     # Getting guarantor
583     my $query = "SELECT guarantorid FROM borrowers WHERE borrowernumber=?";
584     my $sth = $dbh->prepare($query);
585     $sth->execute($borrowernumber);
586     my $data = $sth->fetchrow_arrayref();
587     push @glist, $data->[0] if $data->[0];
588     my $guarantor = $data->[0] ? $data->[0] : undef;
589
590     # Getting guarantees
591     $query = "SELECT borrowernumber FROM borrowers WHERE guarantorid=?";
592     $sth = $dbh->prepare($query);
593     $sth->execute($borrowernumber);
594     while ($data = $sth->fetchrow_arrayref()) {
595        push @glist, $data->[0];
596     }
597
598     # Getting sibling guarantees
599     if ($guarantor) {
600         $query = "SELECT borrowernumber FROM borrowers WHERE guarantorid=?";
601         $sth = $dbh->prepare($query);
602         $sth->execute($guarantor);
603         while ($data = $sth->fetchrow_arrayref()) {
604            push @glist, $data->[0] if ($data->[0] != $borrowernumber);
605         }
606     }
607
608     return @glist;
609 }
610
611 =head2 IsMemberBlocked
612
613   my ($block_status, $count) = IsMemberBlocked( $borrowernumber );
614
615 Returns whether a patron has overdue items that may result
616 in a block or whether the patron has active fine days
617 that would block circulation privileges.
618
619 C<$block_status> can have the following values:
620
621 1 if the patron has outstanding fine days, in which case C<$count> is the number of them
622
623 -1 if the patron has overdue items, in which case C<$count> is the number of them
624
625 0 if the patron has no overdue items or outstanding fine days, in which case C<$count> is 0
626
627 Outstanding fine days are checked before current overdue items
628 are.
629
630 FIXME: this needs to be split into two functions; a potential block
631 based on the number of current overdue items could be orthogonal
632 to a block based on whether the patron has any fine days accrued.
633
634 =cut
635
636 sub IsMemberBlocked {
637     my $borrowernumber = shift;
638     my $dbh            = C4::Context->dbh;
639
640     my $blockeddate = CheckBorrowerDebarred($borrowernumber);
641
642     return ( 1, $blockeddate ) if $blockeddate;
643
644     # if he have late issues
645     my $sth = $dbh->prepare(
646         "SELECT COUNT(*) as latedocs
647          FROM issues
648          WHERE borrowernumber = ?
649          AND date_due < now()"
650     );
651     $sth->execute($borrowernumber);
652     my $latedocs = $sth->fetchrow_hashref->{'latedocs'};
653
654     return ( -1, $latedocs ) if $latedocs > 0;
655
656     return ( 0, 0 );
657 }
658
659 =head2 GetMemberIssuesAndFines
660
661   ($overdue_count, $issue_count, $total_fines) = &GetMemberIssuesAndFines($borrowernumber);
662
663 Returns aggregate data about items borrowed by the patron with the
664 given borrowernumber.
665
666 C<&GetMemberIssuesAndFines> returns a three-element array.  C<$overdue_count> is the
667 number of overdue items the patron currently has borrowed. C<$issue_count> is the
668 number of books the patron currently has borrowed.  C<$total_fines> is
669 the total fine currently due by the borrower.
670
671 =cut
672
673 #'
674 sub GetMemberIssuesAndFines {
675     my ( $borrowernumber ) = @_;
676     my $dbh   = C4::Context->dbh;
677     my $query = "SELECT COUNT(*) FROM issues WHERE borrowernumber = ?";
678
679     $debug and warn $query."\n";
680     my $sth = $dbh->prepare($query);
681     $sth->execute($borrowernumber);
682     my $issue_count = $sth->fetchrow_arrayref->[0];
683
684     $sth = $dbh->prepare(
685         "SELECT COUNT(*) FROM issues 
686          WHERE borrowernumber = ? 
687          AND date_due < now()"
688     );
689     $sth->execute($borrowernumber);
690     my $overdue_count = $sth->fetchrow_arrayref->[0];
691
692     $sth = $dbh->prepare("SELECT SUM(amountoutstanding) FROM accountlines WHERE borrowernumber = ?");
693     $sth->execute($borrowernumber);
694     my $total_fines = $sth->fetchrow_arrayref->[0];
695
696     return ($overdue_count, $issue_count, $total_fines);
697 }
698
699 sub columns(;$) {
700     return @{C4::Context->dbh->selectcol_arrayref("SHOW columns from borrowers")};
701 }
702
703 =head2 ModMember
704
705   my $success = ModMember(borrowernumber => $borrowernumber,
706                                             [ field => value ]... );
707
708 Modify borrower's data.  All date fields should ALREADY be in ISO format.
709
710 return :
711 true on success, or false on failure
712
713 =cut
714
715 sub ModMember {
716     my (%data) = @_;
717     # test to know if you must update or not the borrower password
718     if (exists $data{password}) {
719         if ($data{password} eq '****' or $data{password} eq '') {
720             delete $data{password};
721         } else {
722             $data{password} = md5_base64($data{password});
723         }
724     }
725         my $execute_success=UpdateInTable("borrowers",\%data);
726     if ($execute_success) { # only proceed if the update was a success
727         # ok if its an adult (type) it may have borrowers that depend on it as a guarantor
728         # so when we update information for an adult we should check for guarantees and update the relevant part
729         # of their records, ie addresses and phone numbers
730         my $borrowercategory= GetBorrowercategory( $data{'category_type'} );
731         if ( exists  $borrowercategory->{'category_type'} && $borrowercategory->{'category_type'} eq ('A' || 'S') ) {
732             # is adult check guarantees;
733             UpdateGuarantees(%data);
734         }
735         logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog");
736     }
737     return $execute_success;
738 }
739
740
741 =head2 AddMember
742
743   $borrowernumber = &AddMember(%borrower);
744
745 insert new borrower into table
746 Returns the borrowernumber upon success
747
748 Returns as undef upon any db error without further processing
749
750 =cut
751
752 #'
753 sub AddMember {
754     my (%data) = @_;
755     my $dbh = C4::Context->dbh;
756
757     # generate a proper login if none provided
758     $data{'userid'} = Generate_Userid($data{'borrowernumber'}, $data{'firstname'}, $data{'surname'}) if $data{'userid'} eq '';
759
760     # add expiration date if it isn't already there
761     unless ( $data{'dateexpiry'} ) {
762         $data{'dateexpiry'} = GetExpiryDate( $data{'categorycode'}, C4::Dates->new()->output("iso") );
763     }
764
765     # add enrollment date if it isn't already there
766     unless ( $data{'dateenrolled'} ) {
767         $data{'dateenrolled'} = C4::Dates->new()->output("iso");
768     }
769
770     # create a disabled account if no password provided
771     $data{'password'} = ($data{'password'})? md5_base64($data{'password'}) : '!';
772     $data{'borrowernumber'}=InsertInTable("borrowers",\%data);
773
774
775     # mysql_insertid is probably bad.  not necessarily accurate and mysql-specific at best.
776     logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog");
777     
778     # check for enrollment fee & add it if needed
779     my $sth = $dbh->prepare("SELECT enrolmentfee FROM categories WHERE categorycode=?");
780     $sth->execute($data{'categorycode'});
781     my ($enrolmentfee) = $sth->fetchrow;
782     if ($sth->err) {
783         warn sprintf('Database returned the following error: %s', $sth->errstr);
784         return;
785     }
786     if ($enrolmentfee && $enrolmentfee > 0) {
787         # insert fee in patron debts
788         manualinvoice($data{'borrowernumber'}, '', '', 'A', $enrolmentfee);
789     }
790
791     return $data{'borrowernumber'};
792 }
793
794
795 sub Check_Userid {
796     my ($uid,$member) = @_;
797     my $dbh = C4::Context->dbh;
798     # Make sure the userid chosen is unique and not theirs if non-empty. If it is not,
799     # Then we need to tell the user and have them create a new one.
800     my $sth =
801       $dbh->prepare(
802         "SELECT * FROM borrowers WHERE userid=? AND borrowernumber != ?");
803     $sth->execute( $uid, $member );
804     if ( ( $uid ne '' ) && ( my $row = $sth->fetchrow_hashref ) ) {
805         return 0;
806     }
807     else {
808         return 1;
809     }
810 }
811
812 sub Generate_Userid {
813   my ($borrowernumber, $firstname, $surname) = @_;
814   my $newuid;
815   my $offset = 0;
816   do {
817     $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
818     $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
819     $newuid = lc(($firstname)? "$firstname.$surname" : $surname);
820     $newuid = unac_string('utf-8',$newuid);
821     $newuid .= $offset unless $offset == 0;
822     $offset++;
823
824    } while (!Check_Userid($newuid,$borrowernumber));
825
826    return $newuid;
827 }
828
829 sub changepassword {
830     my ( $uid, $member, $digest ) = @_;
831     my $dbh = C4::Context->dbh;
832
833 #Make sure the userid chosen is unique and not theirs if non-empty. If it is not,
834 #Then we need to tell the user and have them create a new one.
835     my $resultcode;
836     my $sth =
837       $dbh->prepare(
838         "SELECT * FROM borrowers WHERE userid=? AND borrowernumber != ?");
839     $sth->execute( $uid, $member );
840     if ( ( $uid ne '' ) && ( my $row = $sth->fetchrow_hashref ) ) {
841         $resultcode=0;
842     }
843     else {
844         #Everything is good so we can update the information.
845         $sth =
846           $dbh->prepare(
847             "update borrowers set userid=?, password=? where borrowernumber=?");
848         $sth->execute( $uid, $digest, $member );
849         $resultcode=1;
850     }
851     
852     logaction("MEMBERS", "CHANGE PASS", $member, "") if C4::Context->preference("BorrowersLog");
853     return $resultcode;    
854 }
855
856
857
858 =head2 fixup_cardnumber
859
860 Warning: The caller is responsible for locking the members table in write
861 mode, to avoid database corruption.
862
863 =cut
864
865 use vars qw( @weightings );
866 my @weightings = ( 8, 4, 6, 3, 5, 2, 1 );
867
868 sub fixup_cardnumber {
869     my ($cardnumber) = @_;
870     my $autonumber_members = C4::Context->boolean_preference('autoMemberNum') || 0;
871
872     # Find out whether member numbers should be generated
873     # automatically. Should be either "1" or something else.
874     # Defaults to "0", which is interpreted as "no".
875
876     #     if ($cardnumber !~ /\S/ && $autonumber_members) {
877     ($autonumber_members) or return $cardnumber;
878     my $checkdigit = C4::Context->preference('checkdigit');
879     my $dbh = C4::Context->dbh;
880     if ( $checkdigit and $checkdigit eq 'katipo' ) {
881
882         # if checkdigit is selected, calculate katipo-style cardnumber.
883         # otherwise, just use the max()
884         # purpose: generate checksum'd member numbers.
885         # We'll assume we just got the max value of digits 2-8 of member #'s
886         # from the database and our job is to increment that by one,
887         # determine the 1st and 9th digits and return the full string.
888         my $sth = $dbh->prepare(
889             "select max(substring(borrowers.cardnumber,2,7)) as new_num from borrowers"
890         );
891         $sth->execute;
892         my $data = $sth->fetchrow_hashref;
893         $cardnumber = $data->{new_num};
894         if ( !$cardnumber ) {    # If DB has no values,
895             $cardnumber = 1000000;    # start at 1000000
896         } else {
897             $cardnumber += 1;
898         }
899
900         my $sum = 0;
901         for ( my $i = 0 ; $i < 8 ; $i += 1 ) {
902             # read weightings, left to right, 1 char at a time
903             my $temp1 = $weightings[$i];
904
905             # sequence left to right, 1 char at a time
906             my $temp2 = substr( $cardnumber, $i, 1 );
907
908             # mult each char 1-7 by its corresponding weighting
909             $sum += $temp1 * $temp2;
910         }
911
912         my $rem = ( $sum % 11 );
913         $rem = 'X' if $rem == 10;
914
915         return "V$cardnumber$rem";
916      } else {
917
918         my $sth = $dbh->prepare(
919             'SELECT MAX( CAST( cardnumber AS SIGNED ) ) FROM borrowers WHERE cardnumber REGEXP "^-?[0-9]+$"'
920         );
921         $sth->execute;
922         my ($result) = $sth->fetchrow;
923         return $result + 1;
924     }
925     return $cardnumber;     # just here as a fallback/reminder 
926 }
927
928 =head2 GetGuarantees
929
930   ($num_children, $children_arrayref) = &GetGuarantees($parent_borrno);
931   $child0_cardno = $children_arrayref->[0]{"cardnumber"};
932   $child0_borrno = $children_arrayref->[0]{"borrowernumber"};
933
934 C<&GetGuarantees> takes a borrower number (e.g., that of a patron
935 with children) and looks up the borrowers who are guaranteed by that
936 borrower (i.e., the patron's children).
937
938 C<&GetGuarantees> returns two values: an integer giving the number of
939 borrowers guaranteed by C<$parent_borrno>, and a reference to an array
940 of references to hash, which gives the actual results.
941
942 =cut
943
944 #'
945 sub GetGuarantees {
946     my ($borrowernumber) = @_;
947     my $dbh              = C4::Context->dbh;
948     my $sth              =
949       $dbh->prepare(
950 "select cardnumber,borrowernumber, firstname, surname from borrowers where guarantorid=?"
951       );
952     $sth->execute($borrowernumber);
953
954     my @dat;
955     my $data = $sth->fetchall_arrayref({}); 
956     return ( scalar(@$data), $data );
957 }
958
959 =head2 UpdateGuarantees
960
961   &UpdateGuarantees($parent_borrno);
962   
963
964 C<&UpdateGuarantees> borrower data for an adult and updates all the guarantees
965 with the modified information
966
967 =cut
968
969 #'
970 sub UpdateGuarantees {
971     my %data = shift;
972     my $dbh = C4::Context->dbh;
973     my ( $count, $guarantees ) = GetGuarantees( $data{'borrowernumber'} );
974     foreach my $guarantee (@$guarantees){
975         my $guaquery = qq|UPDATE borrowers 
976               SET address=?,fax=?,B_city=?,mobile=?,city=?,phone=?
977               WHERE borrowernumber=?
978         |;
979         my $sth = $dbh->prepare($guaquery);
980         $sth->execute($data{'address'},$data{'fax'},$data{'B_city'},$data{'mobile'},$data{'city'},$data{'phone'},$guarantee->{'borrowernumber'});
981     }
982 }
983 =head2 GetPendingIssues
984
985   my $issues = &GetPendingIssues(@borrowernumber);
986
987 Looks up what the patron with the given borrowernumber has borrowed.
988
989 C<&GetPendingIssues> returns a
990 reference-to-array where each element is a reference-to-hash; the
991 keys are the fields from the C<issues>, C<biblio>, and C<items> tables.
992 The keys include C<biblioitems> fields except marc and marcxml.
993
994 =cut
995
996 #'
997 sub GetPendingIssues {
998     my @borrowernumbers = @_;
999
1000     unless (@borrowernumbers ) { # return a ref_to_array
1001         return \@borrowernumbers; # to not cause surprise to caller
1002     }
1003
1004     # Borrowers part of the query
1005     my $bquery = '';
1006     for (my $i = 0; $i < @borrowernumbers; $i++) {
1007         $bquery .= ' issues.borrowernumber = ?';
1008         if ($i < $#borrowernumbers ) {
1009             $bquery .= ' OR';
1010         }
1011     }
1012
1013     # must avoid biblioitems.* to prevent large marc and marcxml fields from killing performance
1014     # FIXME: namespace collision: each table has "timestamp" fields.  Which one is "timestamp" ?
1015     # FIXME: circ/ciculation.pl tries to sort by timestamp!
1016     # FIXME: namespace collision: other collisions possible.
1017     # FIXME: most of this data isn't really being used by callers.
1018     my $query =
1019    "SELECT issues.*,
1020             items.*,
1021            biblio.*,
1022            biblioitems.volume,
1023            biblioitems.number,
1024            biblioitems.itemtype,
1025            biblioitems.isbn,
1026            biblioitems.issn,
1027            biblioitems.publicationyear,
1028            biblioitems.publishercode,
1029            biblioitems.volumedate,
1030            biblioitems.volumedesc,
1031            biblioitems.lccn,
1032            biblioitems.url,
1033            borrowers.firstname,
1034            borrowers.surname,
1035            borrowers.cardnumber,
1036            issues.timestamp AS timestamp,
1037            issues.renewals  AS renewals,
1038            issues.borrowernumber AS borrowernumber,
1039             items.renewals  AS totalrenewals
1040     FROM   issues
1041     LEFT JOIN items       ON items.itemnumber       =      issues.itemnumber
1042     LEFT JOIN biblio      ON items.biblionumber     =      biblio.biblionumber
1043     LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
1044     LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
1045     WHERE
1046       $bquery
1047     ORDER BY issues.issuedate"
1048     ;
1049
1050     my $sth = C4::Context->dbh->prepare($query);
1051     $sth->execute(@borrowernumbers);
1052     my $data = $sth->fetchall_arrayref({});
1053     my $tz = C4::Context->tz();
1054     my $today = DateTime->now( time_zone => $tz);
1055     foreach (@{$data}) {
1056         if ($_->{issuedate}) {
1057             $_->{issuedate} = dt_from_string($_->{issuedate}, 'sql');
1058         }
1059         $_->{date_due} or next;
1060         $_->{date_due} = DateTime::Format::DateParse->parse_datetime($_->{date_due}, $tz->name());
1061         if ( DateTime->compare($_->{date_due}, $today) == -1 ) {
1062             $_->{overdue} = 1;
1063         }
1064     }
1065     return $data;
1066 }
1067
1068 =head2 GetAllIssues
1069
1070   $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
1071
1072 Looks up what the patron with the given borrowernumber has borrowed,
1073 and sorts the results.
1074
1075 C<$sortkey> is the name of a field on which to sort the results. This
1076 should be the name of a field in the C<issues>, C<biblio>,
1077 C<biblioitems>, or C<items> table in the Koha database.
1078
1079 C<$limit> is the maximum number of results to return.
1080
1081 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
1082 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
1083 C<items> tables of the Koha database.
1084
1085 =cut
1086
1087 #'
1088 sub GetAllIssues {
1089     my ( $borrowernumber, $order, $limit ) = @_;
1090
1091     my $dbh = C4::Context->dbh;
1092     my $query =
1093 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
1094   FROM issues 
1095   LEFT JOIN items on items.itemnumber=issues.itemnumber
1096   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
1097   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
1098   WHERE borrowernumber=? 
1099   UNION ALL
1100   SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp 
1101   FROM old_issues 
1102   LEFT JOIN items on items.itemnumber=old_issues.itemnumber
1103   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
1104   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
1105   WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
1106   order by ' . $order;
1107     if ($limit) {
1108         $query .= " limit $limit";
1109     }
1110
1111     my $sth = $dbh->prepare($query);
1112     $sth->execute( $borrowernumber, $borrowernumber );
1113     return $sth->fetchall_arrayref( {} );
1114 }
1115
1116
1117 =head2 GetMemberAccountRecords
1118
1119   ($total, $acctlines, $count) = &GetMemberAccountRecords($borrowernumber);
1120
1121 Looks up accounting data for the patron with the given borrowernumber.
1122
1123 C<&GetMemberAccountRecords> returns a three-element array. C<$acctlines> is a
1124 reference-to-array, where each element is a reference-to-hash; the
1125 keys are the fields of the C<accountlines> table in the Koha database.
1126 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
1127 total amount outstanding for all of the account lines.
1128
1129 =cut
1130
1131 #'
1132 sub GetMemberAccountRecords {
1133     my ($borrowernumber,$date) = @_;
1134     my $dbh = C4::Context->dbh;
1135     my @acctlines;
1136     my $numlines = 0;
1137     my $strsth      = qq(
1138                         SELECT * 
1139                         FROM accountlines 
1140                         WHERE borrowernumber=?);
1141     my @bind = ($borrowernumber);
1142     if ($date && $date ne ''){
1143             $strsth.=" AND date < ? ";
1144             push(@bind,$date);
1145     }
1146     $strsth.=" ORDER BY date desc,timestamp DESC";
1147     my $sth= $dbh->prepare( $strsth );
1148     $sth->execute( @bind );
1149     my $total = 0;
1150     while ( my $data = $sth->fetchrow_hashref ) {
1151         if ( $data->{itemnumber} ) {
1152             my $biblio = GetBiblioFromItemNumber( $data->{itemnumber} );
1153             $data->{biblionumber} = $biblio->{biblionumber};
1154             $data->{title}        = $biblio->{title};
1155         }
1156         $acctlines[$numlines] = $data;
1157         $numlines++;
1158         $total += int(1000 * $data->{'amountoutstanding'}); # convert float to integer to avoid round-off errors
1159     }
1160     $total /= 1000;
1161     return ( $total, \@acctlines,$numlines);
1162 }
1163
1164 =head2 GetBorNotifyAcctRecord
1165
1166   ($total, $acctlines, $count) = &GetBorNotifyAcctRecord($params,$notifyid);
1167
1168 Looks up accounting data for the patron with the given borrowernumber per file number.
1169
1170 C<&GetBorNotifyAcctRecord> returns a three-element array. C<$acctlines> is a
1171 reference-to-array, where each element is a reference-to-hash; the
1172 keys are the fields of the C<accountlines> table in the Koha database.
1173 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
1174 total amount outstanding for all of the account lines.
1175
1176 =cut
1177
1178 sub GetBorNotifyAcctRecord {
1179     my ( $borrowernumber, $notifyid ) = @_;
1180     my $dbh = C4::Context->dbh;
1181     my @acctlines;
1182     my $numlines = 0;
1183     my $sth = $dbh->prepare(
1184             "SELECT * 
1185                 FROM accountlines 
1186                 WHERE borrowernumber=? 
1187                     AND notify_id=? 
1188                     AND amountoutstanding != '0' 
1189                 ORDER BY notify_id,accounttype
1190                 ");
1191
1192     $sth->execute( $borrowernumber, $notifyid );
1193     my $total = 0;
1194     while ( my $data = $sth->fetchrow_hashref ) {
1195         if ( $data->{itemnumber} ) {
1196             my $biblio = GetBiblioFromItemNumber( $data->{itemnumber} );
1197             $data->{biblionumber} = $biblio->{biblionumber};
1198             $data->{title}        = $biblio->{title};
1199         }
1200         $acctlines[$numlines] = $data;
1201         $numlines++;
1202         $total += int(100 * $data->{'amountoutstanding'});
1203     }
1204     $total /= 100;
1205     return ( $total, \@acctlines, $numlines );
1206 }
1207
1208 =head2 checkuniquemember (OUEST-PROVENCE)
1209
1210   ($result,$categorycode)  = &checkuniquemember($collectivity,$surname,$firstname,$dateofbirth);
1211
1212 Checks that a member exists or not in the database.
1213
1214 C<&result> is nonzero (=exist) or 0 (=does not exist)
1215 C<&categorycode> is from categorycode table
1216 C<&collectivity> is 1 (= we add a collectivity) or 0 (= we add a physical member)
1217 C<&surname> is the surname
1218 C<&firstname> is the firstname (only if collectivity=0)
1219 C<&dateofbirth> is the date of birth in ISO format (only if collectivity=0)
1220
1221 =cut
1222
1223 # FIXME: This function is not legitimate.  Multiple patrons might have the same first/last name and birthdate.
1224 # This is especially true since first name is not even a required field.
1225
1226 sub checkuniquemember {
1227     my ( $collectivity, $surname, $firstname, $dateofbirth ) = @_;
1228     my $dbh = C4::Context->dbh;
1229     my $request = ($collectivity) ?
1230         "SELECT borrowernumber,categorycode FROM borrowers WHERE surname=? " :
1231             ($dateofbirth) ?
1232             "SELECT borrowernumber,categorycode FROM borrowers WHERE surname=? and firstname=?  and dateofbirth=?" :
1233             "SELECT borrowernumber,categorycode FROM borrowers WHERE surname=? and firstname=?";
1234     my $sth = $dbh->prepare($request);
1235     if ($collectivity) {
1236         $sth->execute( uc($surname) );
1237     } elsif($dateofbirth){
1238         $sth->execute( uc($surname), ucfirst($firstname), $dateofbirth );
1239     }else{
1240         $sth->execute( uc($surname), ucfirst($firstname));
1241     }
1242     my @data = $sth->fetchrow;
1243     ( $data[0] ) and return $data[0], $data[1];
1244     return 0;
1245 }
1246
1247 sub checkcardnumber {
1248     my ($cardnumber,$borrowernumber) = @_;
1249     # If cardnumber is null, we assume they're allowed.
1250     return 0 if !defined($cardnumber);
1251     my $dbh = C4::Context->dbh;
1252     my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
1253     $query .= " AND borrowernumber <> ?" if ($borrowernumber);
1254   my $sth = $dbh->prepare($query);
1255   if ($borrowernumber) {
1256    $sth->execute($cardnumber,$borrowernumber);
1257   } else { 
1258      $sth->execute($cardnumber);
1259   } 
1260     if (my $data= $sth->fetchrow_hashref()){
1261         return 1;
1262     }
1263     else {
1264         return 0;
1265     }
1266 }  
1267
1268
1269 =head2 getzipnamecity (OUEST-PROVENCE)
1270
1271 take all info from table city for the fields city and  zip
1272 check for the name and the zip code of the city selected
1273
1274 =cut
1275
1276 sub getzipnamecity {
1277     my ($cityid) = @_;
1278     my $dbh      = C4::Context->dbh;
1279     my $sth      =
1280       $dbh->prepare(
1281         "select city_name,city_state,city_zipcode,city_country from cities where cityid=? ");
1282     $sth->execute($cityid);
1283     my @data = $sth->fetchrow;
1284     return $data[0], $data[1], $data[2], $data[3];
1285 }
1286
1287
1288 =head2 getdcity (OUEST-PROVENCE)
1289
1290 recover cityid  with city_name condition
1291
1292 =cut
1293
1294 sub getidcity {
1295     my ($city_name) = @_;
1296     my $dbh = C4::Context->dbh;
1297     my $sth = $dbh->prepare("select cityid from cities where city_name=? ");
1298     $sth->execute($city_name);
1299     my $data = $sth->fetchrow;
1300     return $data;
1301 }
1302
1303 =head2 GetFirstValidEmailAddress
1304
1305   $email = GetFirstValidEmailAddress($borrowernumber);
1306
1307 Return the first valid email address for a borrower, given the borrowernumber.  For now, the order 
1308 is defined as email, emailpro, B_email.  Returns the empty string if the borrower has no email 
1309 addresses.
1310
1311 =cut
1312
1313 sub GetFirstValidEmailAddress {
1314     my $borrowernumber = shift;
1315     my $dbh = C4::Context->dbh;
1316     my $sth = $dbh->prepare( "SELECT email, emailpro, B_email FROM borrowers where borrowernumber = ? ");
1317     $sth->execute( $borrowernumber );
1318     my $data = $sth->fetchrow_hashref;
1319
1320     if ($data->{'email'}) {
1321        return $data->{'email'};
1322     } elsif ($data->{'emailpro'}) {
1323        return $data->{'emailpro'};
1324     } elsif ($data->{'B_email'}) {
1325        return $data->{'B_email'};
1326     } else {
1327        return '';
1328     }
1329 }
1330
1331 =head2 GetExpiryDate 
1332
1333   $expirydate = GetExpiryDate($categorycode, $dateenrolled);
1334
1335 Calculate expiry date given a categorycode and starting date.  Date argument must be in ISO format.
1336 Return date is also in ISO format.
1337
1338 =cut
1339
1340 sub GetExpiryDate {
1341     my ( $categorycode, $dateenrolled ) = @_;
1342     my $enrolments;
1343     if ($categorycode) {
1344         my $dbh = C4::Context->dbh;
1345         my $sth = $dbh->prepare("SELECT enrolmentperiod,enrolmentperioddate FROM categories WHERE categorycode=?");
1346         $sth->execute($categorycode);
1347         $enrolments = $sth->fetchrow_hashref;
1348     }
1349     # die "GetExpiryDate: for enrollmentperiod $enrolmentperiod (category '$categorycode') starting $dateenrolled.\n";
1350     my @date = split (/-/,$dateenrolled);
1351     if($enrolments->{enrolmentperiod}){
1352         return sprintf("%04d-%02d-%02d", Add_Delta_YM(@date,0,$enrolments->{enrolmentperiod}));
1353     }else{
1354         return $enrolments->{enrolmentperioddate};
1355     }
1356 }
1357
1358 =head2 checkuserpassword (OUEST-PROVENCE)
1359
1360 check for the password and login are not used
1361 return the number of record 
1362 0=> NOT USED 1=> USED
1363
1364 =cut
1365
1366 sub checkuserpassword {
1367     my ( $borrowernumber, $userid, $password ) = @_;
1368     $password = md5_base64($password);
1369     my $dbh = C4::Context->dbh;
1370     my $sth =
1371       $dbh->prepare(
1372 "Select count(*) from borrowers where borrowernumber !=? and userid =? and password=? "
1373       );
1374     $sth->execute( $borrowernumber, $userid, $password );
1375     my $number_rows = $sth->fetchrow;
1376     return $number_rows;
1377
1378 }
1379
1380 =head2 GetborCatFromCatType
1381
1382   ($codes_arrayref, $labels_hashref) = &GetborCatFromCatType();
1383
1384 Looks up the different types of borrowers in the database. Returns two
1385 elements: a reference-to-array, which lists the borrower category
1386 codes, and a reference-to-hash, which maps the borrower category codes
1387 to category descriptions.
1388
1389 =cut
1390
1391 #'
1392 sub GetborCatFromCatType {
1393     my ( $category_type, $action, $no_branch_limit ) = @_;
1394
1395     my $branch_limit = $no_branch_limit
1396         ? 0
1397         : C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1398
1399     # FIXME - This API  seems both limited and dangerous.
1400     my $dbh     = C4::Context->dbh;
1401
1402     my $request = qq{
1403         SELECT categories.categorycode, categories.description
1404         FROM categories
1405     };
1406     $request .= qq{
1407         LEFT JOIN categories_branches ON categories.categorycode = categories_branches.categorycode
1408     } if $branch_limit;
1409     if($action) {
1410         $request .= " $action ";
1411         $request .= " AND (branchcode = ? OR branchcode IS NULL) GROUP BY description" if $branch_limit;
1412     } else {
1413         $request .= " WHERE branchcode = ? OR branchcode IS NULL GROUP BY description" if $branch_limit;
1414     }
1415     $request .= " ORDER BY categorycode";
1416
1417     my $sth = $dbh->prepare($request);
1418     $sth->execute(
1419         $action ? $category_type : (),
1420         $branch_limit ? $branch_limit : ()
1421     );
1422
1423     my %labels;
1424     my @codes;
1425
1426     while ( my $data = $sth->fetchrow_hashref ) {
1427         push @codes, $data->{'categorycode'};
1428         $labels{ $data->{'categorycode'} } = $data->{'description'};
1429     }
1430     $sth->finish;
1431     return ( \@codes, \%labels );
1432 }
1433
1434 =head2 GetBorrowercategory
1435
1436   $hashref = &GetBorrowercategory($categorycode);
1437
1438 Given the borrower's category code, the function returns the corresponding
1439 data hashref for a comprehensive information display.
1440
1441 =cut
1442
1443 sub GetBorrowercategory {
1444     my ($catcode) = @_;
1445     my $dbh       = C4::Context->dbh;
1446     if ($catcode){
1447         my $sth       =
1448         $dbh->prepare(
1449     "SELECT description,dateofbirthrequired,upperagelimit,category_type 
1450     FROM categories 
1451     WHERE categorycode = ?"
1452         );
1453         $sth->execute($catcode);
1454         my $data =
1455         $sth->fetchrow_hashref;
1456         return $data;
1457     } 
1458     return;  
1459 }    # sub getborrowercategory
1460
1461
1462 =head2 GetBorrowerCategorycode
1463
1464     $categorycode = &GetBorrowerCategoryCode( $borrowernumber );
1465
1466 Given the borrowernumber, the function returns the corresponding categorycode
1467 =cut
1468
1469 sub GetBorrowerCategorycode {
1470     my ( $borrowernumber ) = @_;
1471     my $dbh = C4::Context->dbh;
1472     my $sth = $dbh->prepare( qq{
1473         SELECT categorycode
1474         FROM borrowers
1475         WHERE borrowernumber = ?
1476     } );
1477     $sth->execute( $borrowernumber );
1478     return $sth->fetchrow;
1479 }
1480
1481 =head2 GetBorrowercategoryList
1482
1483   $arrayref_hashref = &GetBorrowercategoryList;
1484 If no category code provided, the function returns all the categories.
1485
1486 =cut
1487
1488 sub GetBorrowercategoryList {
1489     my $no_branch_limit = @_ ? shift : 0;
1490     my $branch_limit = $no_branch_limit
1491         ? 0
1492         : C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1493     my $dbh       = C4::Context->dbh;
1494     my $query = "SELECT categories.* FROM categories";
1495     $query .= qq{
1496         LEFT JOIN categories_branches ON categories.categorycode = categories_branches.categorycode
1497         WHERE branchcode = ? OR branchcode IS NULL GROUP BY description
1498     } if $branch_limit;
1499     $query .= " ORDER BY description";
1500     my $sth = $dbh->prepare( $query );
1501     $sth->execute( $branch_limit ? $branch_limit : () );
1502     my $data = $sth->fetchall_arrayref( {} );
1503     $sth->finish;
1504     return $data;
1505 }    # sub getborrowercategory
1506
1507 =head2 ethnicitycategories
1508
1509   ($codes_arrayref, $labels_hashref) = &ethnicitycategories();
1510
1511 Looks up the different ethnic types in the database. Returns two
1512 elements: a reference-to-array, which lists the ethnicity codes, and a
1513 reference-to-hash, which maps the ethnicity codes to ethnicity
1514 descriptions.
1515
1516 =cut
1517
1518 #'
1519
1520 sub ethnicitycategories {
1521     my $dbh = C4::Context->dbh;
1522     my $sth = $dbh->prepare("Select code,name from ethnicity order by name");
1523     $sth->execute;
1524     my %labels;
1525     my @codes;
1526     while ( my $data = $sth->fetchrow_hashref ) {
1527         push @codes, $data->{'code'};
1528         $labels{ $data->{'code'} } = $data->{'name'};
1529     }
1530     return ( \@codes, \%labels );
1531 }
1532
1533 =head2 fixEthnicity
1534
1535   $ethn_name = &fixEthnicity($ethn_code);
1536
1537 Takes an ethnicity code (e.g., "european" or "pi") and returns the
1538 corresponding descriptive name from the C<ethnicity> table in the
1539 Koha database ("European" or "Pacific Islander").
1540
1541 =cut
1542
1543 #'
1544
1545 sub fixEthnicity {
1546     my $ethnicity = shift;
1547     return unless $ethnicity;
1548     my $dbh       = C4::Context->dbh;
1549     my $sth       = $dbh->prepare("Select name from ethnicity where code = ?");
1550     $sth->execute($ethnicity);
1551     my $data = $sth->fetchrow_hashref;
1552     return $data->{'name'};
1553 }    # sub fixEthnicity
1554
1555 =head2 GetAge
1556
1557   $dateofbirth,$date = &GetAge($date);
1558
1559 this function return the borrowers age with the value of dateofbirth
1560
1561 =cut
1562
1563 #'
1564 sub GetAge{
1565     my ( $date, $date_ref ) = @_;
1566
1567     if ( not defined $date_ref ) {
1568         $date_ref = sprintf( '%04d-%02d-%02d', Today() );
1569     }
1570
1571     my ( $year1, $month1, $day1 ) = split /-/, $date;
1572     my ( $year2, $month2, $day2 ) = split /-/, $date_ref;
1573
1574     my $age = $year2 - $year1;
1575     if ( $month1 . $day1 > $month2 . $day2 ) {
1576         $age--;
1577     }
1578
1579     return $age;
1580 }    # sub get_age
1581
1582 =head2 get_institutions
1583
1584   $insitutions = get_institutions();
1585
1586 Just returns a list of all the borrowers of type I, borrownumber and name
1587
1588 =cut
1589
1590 #'
1591 sub get_institutions {
1592     my $dbh = C4::Context->dbh();
1593     my $sth =
1594       $dbh->prepare(
1595 "SELECT borrowernumber,surname FROM borrowers WHERE categorycode=? ORDER BY surname"
1596       );
1597     $sth->execute('I');
1598     my %orgs;
1599     while ( my $data = $sth->fetchrow_hashref() ) {
1600         $orgs{ $data->{'borrowernumber'} } = $data;
1601     }
1602     return ( \%orgs );
1603
1604 }    # sub get_institutions
1605
1606 =head2 add_member_orgs
1607
1608   add_member_orgs($borrowernumber,$borrowernumbers);
1609
1610 Takes a borrowernumber and a list of other borrowernumbers and inserts them into the borrowers_to_borrowers table
1611
1612 =cut
1613
1614 #'
1615 sub add_member_orgs {
1616     my ( $borrowernumber, $otherborrowers ) = @_;
1617     my $dbh   = C4::Context->dbh();
1618     my $query =
1619       "INSERT INTO borrowers_to_borrowers (borrower1,borrower2) VALUES (?,?)";
1620     my $sth = $dbh->prepare($query);
1621     foreach my $otherborrowernumber (@$otherborrowers) {
1622         $sth->execute( $borrowernumber, $otherborrowernumber );
1623     }
1624
1625 }    # sub add_member_orgs
1626
1627 =head2 GetCities
1628
1629   $cityarrayref = GetCities();
1630
1631   Returns an array_ref of the entries in the cities table
1632   If there are entries in the table an empty row is returned
1633   This is currently only used to populate a popup in memberentry
1634
1635 =cut
1636
1637 sub GetCities {
1638
1639     my $dbh   = C4::Context->dbh;
1640     my $city_arr = $dbh->selectall_arrayref(
1641         q|SELECT cityid,city_zipcode,city_name,city_state,city_country FROM cities ORDER BY city_name|,
1642         { Slice => {} });
1643     if ( @{$city_arr} ) {
1644         unshift @{$city_arr}, {
1645             city_zipcode => q{},
1646             city_name    => q{},
1647             cityid       => q{},
1648             city_state   => q{},
1649             city_country => q{},
1650         };
1651     }
1652
1653     return  $city_arr;
1654 }
1655
1656 =head2 GetSortDetails (OUEST-PROVENCE)
1657
1658   ($lib) = &GetSortDetails($category,$sortvalue);
1659
1660 Returns the authorized value  details
1661 C<&$lib>return value of authorized value details
1662 C<&$sortvalue>this is the value of authorized value 
1663 C<&$category>this is the value of authorized value category
1664
1665 =cut
1666
1667 sub GetSortDetails {
1668     my ( $category, $sortvalue ) = @_;
1669     my $dbh   = C4::Context->dbh;
1670     my $query = qq|SELECT lib 
1671         FROM authorised_values 
1672         WHERE category=?
1673         AND authorised_value=? |;
1674     my $sth = $dbh->prepare($query);
1675     $sth->execute( $category, $sortvalue );
1676     my $lib = $sth->fetchrow;
1677     return ($lib) if ($lib);
1678     return ($sortvalue) unless ($lib);
1679 }
1680
1681 =head2 MoveMemberToDeleted
1682
1683   $result = &MoveMemberToDeleted($borrowernumber);
1684
1685 Copy the record from borrowers to deletedborrowers table.
1686
1687 =cut
1688
1689 # FIXME: should do it in one SQL statement w/ subquery
1690 # Otherwise, we should return the @data on success
1691
1692 sub MoveMemberToDeleted {
1693     my ($member) = shift or return;
1694     my $dbh = C4::Context->dbh;
1695     my $query = qq|SELECT * 
1696           FROM borrowers 
1697           WHERE borrowernumber=?|;
1698     my $sth = $dbh->prepare($query);
1699     $sth->execute($member);
1700     my @data = $sth->fetchrow_array;
1701     (@data) or return;  # if we got a bad borrowernumber, there's nothing to insert
1702     $sth =
1703       $dbh->prepare( "INSERT INTO deletedborrowers VALUES ("
1704           . ( "?," x ( scalar(@data) - 1 ) )
1705           . "?)" );
1706     $sth->execute(@data);
1707 }
1708
1709 =head2 DelMember
1710
1711     DelMember($borrowernumber);
1712
1713 This function remove directly a borrower whitout writing it on deleteborrower.
1714 + Deletes reserves for the borrower
1715
1716 =cut
1717
1718 sub DelMember {
1719     my $dbh            = C4::Context->dbh;
1720     my $borrowernumber = shift;
1721     #warn "in delmember with $borrowernumber";
1722     return unless $borrowernumber;    # borrowernumber is mandatory.
1723
1724     my $query = qq|DELETE 
1725           FROM  reserves 
1726           WHERE borrowernumber=?|;
1727     my $sth = $dbh->prepare($query);
1728     $sth->execute($borrowernumber);
1729     $query = "
1730        DELETE
1731        FROM borrowers
1732        WHERE borrowernumber = ?
1733    ";
1734     $sth = $dbh->prepare($query);
1735     $sth->execute($borrowernumber);
1736     logaction("MEMBERS", "DELETE", $borrowernumber, "") if C4::Context->preference("BorrowersLog");
1737     return $sth->rows;
1738 }
1739
1740 =head2 ExtendMemberSubscriptionTo (OUEST-PROVENCE)
1741
1742     $date = ExtendMemberSubscriptionTo($borrowerid, $date);
1743
1744 Extending the subscription to a given date or to the expiry date calculated on ISO date.
1745 Returns ISO date.
1746
1747 =cut
1748
1749 sub ExtendMemberSubscriptionTo {
1750     my ( $borrowerid,$date) = @_;
1751     my $dbh = C4::Context->dbh;
1752     my $borrower = GetMember('borrowernumber'=>$borrowerid);
1753     unless ($date){
1754       $date = (C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry') ?
1755                                         C4::Dates->new($borrower->{'dateexpiry'}, 'iso')->output("iso") :
1756                                         C4::Dates->new()->output("iso");
1757       $date = GetExpiryDate( $borrower->{'categorycode'}, $date );
1758     }
1759     my $sth = $dbh->do(<<EOF);
1760 UPDATE borrowers 
1761 SET  dateexpiry='$date' 
1762 WHERE borrowernumber='$borrowerid'
1763 EOF
1764     # add enrolmentfee if needed
1765     $sth = $dbh->prepare("SELECT enrolmentfee FROM categories WHERE categorycode=?");
1766     $sth->execute($borrower->{'categorycode'});
1767     my ($enrolmentfee) = $sth->fetchrow;
1768     if ($enrolmentfee && $enrolmentfee > 0) {
1769         # insert fee in patron debts
1770         manualinvoice($borrower->{'borrowernumber'}, '', '', 'A', $enrolmentfee);
1771     }
1772      logaction("MEMBERS", "RENEW", $borrower->{'borrowernumber'}, "Membership renewed")if C4::Context->preference("BorrowersLog");
1773     return $date if ($sth);
1774     return 0;
1775 }
1776
1777 =head2 GetRoadTypes (OUEST-PROVENCE)
1778
1779   ($idroadtypearrayref, $roadttype_hashref) = &GetRoadTypes();
1780
1781 Looks up the different road type . Returns two
1782 elements: a reference-to-array, which lists the id_roadtype
1783 codes, and a reference-to-hash, which maps the road type of the road .
1784
1785 =cut
1786
1787 sub GetRoadTypes {
1788     my $dbh   = C4::Context->dbh;
1789     my $query = qq|
1790 SELECT roadtypeid,road_type 
1791 FROM roadtype 
1792 ORDER BY road_type|;
1793     my $sth = $dbh->prepare($query);
1794     $sth->execute();
1795     my %roadtype;
1796     my @id;
1797
1798     #    insert empty value to create a empty choice in cgi popup
1799
1800     while ( my $data = $sth->fetchrow_hashref ) {
1801
1802         push @id, $data->{'roadtypeid'};
1803         $roadtype{ $data->{'roadtypeid'} } = $data->{'road_type'};
1804     }
1805
1806 #test to know if the table contain some records if no the function return nothing
1807     my $id = @id;
1808     if ( $id eq 0 ) {
1809         return ();
1810     }
1811     else {
1812         unshift( @id, "" );
1813         return ( \@id, \%roadtype );
1814     }
1815 }
1816
1817
1818
1819 =head2 GetTitles (OUEST-PROVENCE)
1820
1821   ($borrowertitle)= &GetTitles();
1822
1823 Looks up the different title . Returns array  with all borrowers title
1824
1825 =cut
1826
1827 sub GetTitles {
1828     my @borrowerTitle = split (/,|\|/,C4::Context->preference('BorrowersTitles'));
1829     unshift( @borrowerTitle, "" );
1830     my $count=@borrowerTitle;
1831     if ($count == 1){
1832         return ();
1833     }
1834     else {
1835         return ( \@borrowerTitle);
1836     }
1837 }
1838
1839 =head2 GetPatronImage
1840
1841     my ($imagedata, $dberror) = GetPatronImage($cardnumber);
1842
1843 Returns the mimetype and binary image data of the image for the patron with the supplied cardnumber.
1844
1845 =cut
1846
1847 sub GetPatronImage {
1848     my ($cardnumber) = @_;
1849     warn "Cardnumber passed to GetPatronImage is $cardnumber" if $debug;
1850     my $dbh = C4::Context->dbh;
1851     my $query = 'SELECT mimetype, imagefile FROM patronimage WHERE cardnumber = ?';
1852     my $sth = $dbh->prepare($query);
1853     $sth->execute($cardnumber);
1854     my $imagedata = $sth->fetchrow_hashref;
1855     warn "Database error!" if $sth->errstr;
1856     return $imagedata, $sth->errstr;
1857 }
1858
1859 =head2 PutPatronImage
1860
1861     PutPatronImage($cardnumber, $mimetype, $imgfile);
1862
1863 Stores patron binary image data and mimetype in database.
1864 NOTE: This function is good for updating images as well as inserting new images in the database.
1865
1866 =cut
1867
1868 sub PutPatronImage {
1869     my ($cardnumber, $mimetype, $imgfile) = @_;
1870     warn "Parameters passed in: Cardnumber=$cardnumber, Mimetype=$mimetype, " . ($imgfile ? "Imagefile" : "No Imagefile") if $debug;
1871     my $dbh = C4::Context->dbh;
1872     my $query = "INSERT INTO patronimage (cardnumber, mimetype, imagefile) VALUES (?,?,?) ON DUPLICATE KEY UPDATE imagefile = ?;";
1873     my $sth = $dbh->prepare($query);
1874     $sth->execute($cardnumber,$mimetype,$imgfile,$imgfile);
1875     warn "Error returned inserting $cardnumber.$mimetype." if $sth->errstr;
1876     return $sth->errstr;
1877 }
1878
1879 =head2 RmPatronImage
1880
1881     my ($dberror) = RmPatronImage($cardnumber);
1882
1883 Removes the image for the patron with the supplied cardnumber.
1884
1885 =cut
1886
1887 sub RmPatronImage {
1888     my ($cardnumber) = @_;
1889     warn "Cardnumber passed to GetPatronImage is $cardnumber" if $debug;
1890     my $dbh = C4::Context->dbh;
1891     my $query = "DELETE FROM patronimage WHERE cardnumber = ?;";
1892     my $sth = $dbh->prepare($query);
1893     $sth->execute($cardnumber);
1894     my $dberror = $sth->errstr;
1895     warn "Database error!" if $sth->errstr;
1896     return $dberror;
1897 }
1898
1899 =head2 GetHideLostItemsPreference
1900
1901   $hidelostitemspref = &GetHideLostItemsPreference($borrowernumber);
1902
1903 Returns the HideLostItems preference for the patron category of the supplied borrowernumber
1904 C<&$hidelostitemspref>return value of function, 0 or 1
1905
1906 =cut
1907
1908 sub GetHideLostItemsPreference {
1909     my ($borrowernumber) = @_;
1910     my $dbh = C4::Context->dbh;
1911     my $query = "SELECT hidelostitems FROM borrowers,categories WHERE borrowers.categorycode = categories.categorycode AND borrowernumber = ?";
1912     my $sth = $dbh->prepare($query);
1913     $sth->execute($borrowernumber);
1914     my $hidelostitems = $sth->fetchrow;    
1915     return $hidelostitems;    
1916 }
1917
1918 =head2 GetRoadTypeDetails (OUEST-PROVENCE)
1919
1920   ($roadtype) = &GetRoadTypeDetails($roadtypeid);
1921
1922 Returns the description of roadtype
1923 C<&$roadtype>return description of road type
1924 C<&$roadtypeid>this is the value of roadtype s
1925
1926 =cut
1927
1928 sub GetRoadTypeDetails {
1929     my ($roadtypeid) = @_;
1930     my $dbh          = C4::Context->dbh;
1931     my $query        = qq|
1932 SELECT road_type 
1933 FROM roadtype 
1934 WHERE roadtypeid=?|;
1935     my $sth = $dbh->prepare($query);
1936     $sth->execute($roadtypeid);
1937     my $roadtype = $sth->fetchrow;
1938     return ($roadtype);
1939 }
1940
1941 =head2 GetBorrowersToExpunge
1942
1943   $borrowers = &GetBorrowersToExpunge(
1944       not_borrowered_since => $not_borrowered_since,
1945       expired_before       => $expired_before,
1946       category_code        => $category_code,
1947       branchcode           => $branchcode
1948   );
1949
1950   This function get all borrowers based on the given criteria.
1951
1952 =cut
1953
1954 sub GetBorrowersToExpunge {
1955     my $params = shift;
1956
1957     my $filterdate     = $params->{'not_borrowered_since'};
1958     my $filterexpiry   = $params->{'expired_before'};
1959     my $filtercategory = $params->{'category_code'};
1960     my $filterbranch   = $params->{'branchcode'} ||
1961                         ((C4::Context->preference('IndependantBranches') 
1962                              && C4::Context->userenv 
1963                              && C4::Context->userenv->{flags} % 2 !=1 
1964                              && C4::Context->userenv->{branch})
1965                          ? C4::Context->userenv->{branch}
1966                          : "");  
1967
1968     my $dbh   = C4::Context->dbh;
1969     my $query = "
1970         SELECT borrowers.borrowernumber,
1971                MAX(old_issues.timestamp) AS latestissue,
1972                MAX(issues.timestamp) AS currentissue
1973         FROM   borrowers
1974         JOIN   categories USING (categorycode)
1975         LEFT JOIN old_issues USING (borrowernumber)
1976         LEFT JOIN issues USING (borrowernumber) 
1977         WHERE  category_type <> 'S'
1978         AND borrowernumber NOT IN (SELECT guarantorid FROM borrowers WHERE guarantorid IS NOT NULL AND guarantorid <> 0)
1979    ";
1980     my @query_params;
1981     if ( $filterbranch && $filterbranch ne "" ) {
1982         $query.= " AND borrowers.branchcode = ? ";
1983         push( @query_params, $filterbranch );
1984     }
1985     if ( $filterexpiry ) {
1986         $query .= " AND dateexpiry < ? ";
1987         push( @query_params, $filterexpiry );
1988     }
1989     if ( $filtercategory ) {
1990         $query .= " AND categorycode = ? ";
1991         push( @query_params, $filtercategory );
1992     }
1993     $query.=" GROUP BY borrowers.borrowernumber HAVING currentissue IS NULL ";
1994     if ( $filterdate ) {
1995         $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
1996         push @query_params,$filterdate;
1997     }
1998     warn $query if $debug;
1999
2000     my $sth = $dbh->prepare($query);
2001     if (scalar(@query_params)>0){  
2002         $sth->execute(@query_params);
2003     } 
2004     else {
2005         $sth->execute;
2006     }      
2007     
2008     my @results;
2009     while ( my $data = $sth->fetchrow_hashref ) {
2010         push @results, $data;
2011     }
2012     return \@results;
2013 }
2014
2015 =head2 GetBorrowersWhoHaveNeverBorrowed
2016
2017   $results = &GetBorrowersWhoHaveNeverBorrowed
2018
2019 This function get all borrowers who have never borrowed.
2020
2021 I<$result> is a ref to an array which all elements are a hasref.
2022
2023 =cut
2024
2025 sub GetBorrowersWhoHaveNeverBorrowed {
2026     my $filterbranch = shift || 
2027                         ((C4::Context->preference('IndependantBranches') 
2028                              && C4::Context->userenv 
2029                              && C4::Context->userenv->{flags} % 2 !=1 
2030                              && C4::Context->userenv->{branch})
2031                          ? C4::Context->userenv->{branch}
2032                          : "");  
2033     my $dbh   = C4::Context->dbh;
2034     my $query = "
2035         SELECT borrowers.borrowernumber,max(timestamp) as latestissue
2036         FROM   borrowers
2037           LEFT JOIN issues ON borrowers.borrowernumber = issues.borrowernumber
2038         WHERE issues.borrowernumber IS NULL
2039    ";
2040     my @query_params;
2041     if ($filterbranch && $filterbranch ne ""){ 
2042         $query.=" AND borrowers.branchcode= ?";
2043         push @query_params,$filterbranch;
2044     }
2045     warn $query if $debug;
2046   
2047     my $sth = $dbh->prepare($query);
2048     if (scalar(@query_params)>0){  
2049         $sth->execute(@query_params);
2050     } 
2051     else {
2052         $sth->execute;
2053     }      
2054     
2055     my @results;
2056     while ( my $data = $sth->fetchrow_hashref ) {
2057         push @results, $data;
2058     }
2059     return \@results;
2060 }
2061
2062 =head2 GetBorrowersWithIssuesHistoryOlderThan
2063
2064   $results = &GetBorrowersWithIssuesHistoryOlderThan($date)
2065
2066 this function get all borrowers who has an issue history older than I<$date> given on input arg.
2067
2068 I<$result> is a ref to an array which all elements are a hashref.
2069 This hashref is containt the number of time this borrowers has borrowed before I<$date> and the borrowernumber.
2070
2071 =cut
2072
2073 sub GetBorrowersWithIssuesHistoryOlderThan {
2074     my $dbh  = C4::Context->dbh;
2075     my $date = shift ||POSIX::strftime("%Y-%m-%d",localtime());
2076     my $filterbranch = shift || 
2077                         ((C4::Context->preference('IndependantBranches') 
2078                              && C4::Context->userenv 
2079                              && C4::Context->userenv->{flags} % 2 !=1 
2080                              && C4::Context->userenv->{branch})
2081                          ? C4::Context->userenv->{branch}
2082                          : "");  
2083     my $query = "
2084        SELECT count(borrowernumber) as n,borrowernumber
2085        FROM old_issues
2086        WHERE returndate < ?
2087          AND borrowernumber IS NOT NULL 
2088     "; 
2089     my @query_params;
2090     push @query_params, $date;
2091     if ($filterbranch){
2092         $query.="   AND branchcode = ?";
2093         push @query_params, $filterbranch;
2094     }    
2095     $query.=" GROUP BY borrowernumber ";
2096     warn $query if $debug;
2097     my $sth = $dbh->prepare($query);
2098     $sth->execute(@query_params);
2099     my @results;
2100
2101     while ( my $data = $sth->fetchrow_hashref ) {
2102         push @results, $data;
2103     }
2104     return \@results;
2105 }
2106
2107 =head2 GetBorrowersNamesAndLatestIssue
2108
2109   $results = &GetBorrowersNamesAndLatestIssueList(@borrowernumbers)
2110
2111 this function get borrowers Names and surnames and Issue information.
2112
2113 I<@borrowernumbers> is an array which all elements are borrowernumbers.
2114 This hashref is containt the number of time this borrowers has borrowed before I<$date> and the borrowernumber.
2115
2116 =cut
2117
2118 sub GetBorrowersNamesAndLatestIssue {
2119     my $dbh  = C4::Context->dbh;
2120     my @borrowernumbers=@_;  
2121     my $query = "
2122        SELECT surname,lastname, phone, email,max(timestamp)
2123        FROM borrowers 
2124          LEFT JOIN issues ON borrowers.borrowernumber=issues.borrowernumber
2125        GROUP BY borrowernumber
2126    ";
2127     my $sth = $dbh->prepare($query);
2128     $sth->execute;
2129     my $results = $sth->fetchall_arrayref({});
2130     return $results;
2131 }
2132
2133 =head2 DebarMember
2134
2135 my $success = DebarMember( $borrowernumber, $todate );
2136
2137 marks a Member as debarred, and therefore unable to checkout any more
2138 items.
2139
2140 return :
2141 true on success, false on failure
2142
2143 =cut
2144
2145 sub DebarMember {
2146     my $borrowernumber = shift;
2147     my $todate         = shift;
2148
2149     return unless defined $borrowernumber;
2150     return unless $borrowernumber =~ /^\d+$/;
2151
2152     return ModMember(
2153         borrowernumber => $borrowernumber,
2154         debarred       => $todate
2155     );
2156
2157 }
2158
2159 =head2 ModPrivacy
2160
2161 =over 4
2162
2163 my $success = ModPrivacy( $borrowernumber, $privacy );
2164
2165 Update the privacy of a patron.
2166
2167 return :
2168 true on success, false on failure
2169
2170 =back
2171
2172 =cut
2173
2174 sub ModPrivacy {
2175     my $borrowernumber = shift;
2176     my $privacy = shift;
2177     return unless defined $borrowernumber;
2178     return unless $borrowernumber =~ /^\d+$/;
2179
2180     return ModMember( borrowernumber => $borrowernumber,
2181                       privacy        => $privacy );
2182 }
2183
2184 =head2 AddMessage
2185
2186   AddMessage( $borrowernumber, $message_type, $message, $branchcode );
2187
2188 Adds a message to the messages table for the given borrower.
2189
2190 Returns:
2191   True on success
2192   False on failure
2193
2194 =cut
2195
2196 sub AddMessage {
2197     my ( $borrowernumber, $message_type, $message, $branchcode ) = @_;
2198
2199     my $dbh  = C4::Context->dbh;
2200
2201     if ( ! ( $borrowernumber && $message_type && $message && $branchcode ) ) {
2202       return;
2203     }
2204
2205     my $query = "INSERT INTO messages ( borrowernumber, branchcode, message_type, message ) VALUES ( ?, ?, ?, ? )";
2206     my $sth = $dbh->prepare($query);
2207     $sth->execute( $borrowernumber, $branchcode, $message_type, $message );
2208     logaction("MEMBERS", "ADDCIRCMESSAGE", $borrowernumber, $message) if C4::Context->preference("BorrowersLog");
2209     return 1;
2210 }
2211
2212 =head2 GetMessages
2213
2214   GetMessages( $borrowernumber, $type );
2215
2216 $type is message type, B for borrower, or L for Librarian.
2217 Empty type returns all messages of any type.
2218
2219 Returns all messages for the given borrowernumber
2220
2221 =cut
2222
2223 sub GetMessages {
2224     my ( $borrowernumber, $type, $branchcode ) = @_;
2225
2226     if ( ! $type ) {
2227       $type = '%';
2228     }
2229
2230     my $dbh  = C4::Context->dbh;
2231
2232     my $query = "SELECT
2233                   branches.branchname,
2234                   messages.*,
2235                   message_date,
2236                   messages.branchcode LIKE '$branchcode' AS can_delete
2237                   FROM messages, branches
2238                   WHERE borrowernumber = ?
2239                   AND message_type LIKE ?
2240                   AND messages.branchcode = branches.branchcode
2241                   ORDER BY message_date DESC";
2242     my $sth = $dbh->prepare($query);
2243     $sth->execute( $borrowernumber, $type ) ;
2244     my @results;
2245
2246     while ( my $data = $sth->fetchrow_hashref ) {
2247         my $d = C4::Dates->new( $data->{message_date}, 'iso' );
2248         $data->{message_date_formatted} = $d->output;
2249         push @results, $data;
2250     }
2251     return \@results;
2252
2253 }
2254
2255 =head2 GetMessages
2256
2257   GetMessagesCount( $borrowernumber, $type );
2258
2259 $type is message type, B for borrower, or L for Librarian.
2260 Empty type returns all messages of any type.
2261
2262 Returns the number of messages for the given borrowernumber
2263
2264 =cut
2265
2266 sub GetMessagesCount {
2267     my ( $borrowernumber, $type, $branchcode ) = @_;
2268
2269     if ( ! $type ) {
2270       $type = '%';
2271     }
2272
2273     my $dbh  = C4::Context->dbh;
2274
2275     my $query = "SELECT COUNT(*) as MsgCount FROM messages WHERE borrowernumber = ? AND message_type LIKE ?";
2276     my $sth = $dbh->prepare($query);
2277     $sth->execute( $borrowernumber, $type ) ;
2278     my @results;
2279
2280     my $data = $sth->fetchrow_hashref;
2281     my $count = $data->{'MsgCount'};
2282
2283     return $count;
2284 }
2285
2286
2287
2288 =head2 DeleteMessage
2289
2290   DeleteMessage( $message_id );
2291
2292 =cut
2293
2294 sub DeleteMessage {
2295     my ( $message_id ) = @_;
2296
2297     my $dbh = C4::Context->dbh;
2298     my $query = "SELECT * FROM messages WHERE message_id = ?";
2299     my $sth = $dbh->prepare($query);
2300     $sth->execute( $message_id );
2301     my $message = $sth->fetchrow_hashref();
2302
2303     $query = "DELETE FROM messages WHERE message_id = ?";
2304     $sth = $dbh->prepare($query);
2305     $sth->execute( $message_id );
2306     logaction("MEMBERS", "DELCIRCMESSAGE", $message->{'borrowernumber'}, $message->{'message'}) if C4::Context->preference("BorrowersLog");
2307 }
2308
2309 =head2 IssueSlip
2310
2311   IssueSlip($branchcode, $borrowernumber, $quickslip)
2312
2313   Returns letter hash ( see C4::Letters::GetPreparedLetter )
2314
2315   $quickslip is boolean, to indicate whether we want a quick slip
2316
2317 =cut
2318
2319 sub IssueSlip {
2320     my ($branch, $borrowernumber, $quickslip) = @_;
2321
2322 #   return unless ( C4::Context->boolean_preference('printcirculationslips') );
2323
2324     my $now       = POSIX::strftime("%Y-%m-%d", localtime);
2325
2326     my $issueslist = GetPendingIssues($borrowernumber);
2327     foreach my $it (@$issueslist){
2328         if ((substr $it->{'issuedate'}, 0, 10) eq $now) {
2329             $it->{'now'} = 1;
2330         }
2331         elsif ((substr $it->{'date_due'}, 0, 10) le $now) {
2332             $it->{'overdue'} = 1;
2333         }
2334
2335         $it->{'date_due'}=format_date($it->{'date_due'});
2336     }
2337     my @issues = sort { $b->{'timestamp'} <=> $a->{'timestamp'} } @$issueslist;
2338
2339     my ($letter_code, %repeat);
2340     if ( $quickslip ) {
2341         $letter_code = 'ISSUEQSLIP';
2342         %repeat =  (
2343             'checkedout' => [ map {
2344                 'biblio' => $_,
2345                 'items'  => $_,
2346                 'issues' => $_,
2347             }, grep { $_->{'now'} } @issues ],
2348         );
2349     }
2350     else {
2351         $letter_code = 'ISSUESLIP';
2352         %repeat =  (
2353             'checkedout' => [ map {
2354                 'biblio' => $_,
2355                 'items'  => $_,
2356                 'issues' => $_,
2357             }, grep { !$_->{'overdue'} } @issues ],
2358
2359             'overdue' => [ map {
2360                 'biblio' => $_,
2361                 'items'  => $_,
2362                 'issues' => $_,
2363             }, grep { $_->{'overdue'} } @issues ],
2364
2365             'news' => [ map {
2366                 $_->{'timestamp'} = $_->{'newdate'};
2367                 { opac_news => $_ }
2368             } @{ GetNewsToDisplay("slip") } ],
2369         );
2370     }
2371
2372     return  C4::Letters::GetPreparedLetter (
2373         module => 'circulation',
2374         letter_code => $letter_code,
2375         branchcode => $branch,
2376         tables => {
2377             'branches'    => $branch,
2378             'borrowers'   => $borrowernumber,
2379         },
2380         repeat => \%repeat,
2381     );
2382 }
2383
2384 =head2 GetBorrowersWithEmail
2385
2386     ([$borrnum,$userid], ...) = GetBorrowersWithEmail('me@example.com');
2387
2388 This gets a list of users and their basic details from their email address.
2389 As it's possible for multiple user to have the same email address, it provides
2390 you with all of them. If there is no userid for the user, there will be an
2391 C<undef> there. An empty list will be returned if there are no matches.
2392
2393 =cut
2394
2395 sub GetBorrowersWithEmail {
2396     my $email = shift;
2397
2398     my $dbh = C4::Context->dbh;
2399
2400     my $query = "SELECT borrowernumber, userid FROM borrowers WHERE email=?";
2401     my $sth=$dbh->prepare($query);
2402     $sth->execute($email);
2403     my @result = ();
2404     while (my $ref = $sth->fetch) {
2405         push @result, $ref;
2406     }
2407     die "Failure searching for borrowers by email address: $sth->errstr" if $sth->err;
2408     return @result;
2409 }
2410
2411 sub AddMember_Opac {
2412     my ( %borrower ) = @_;
2413
2414     $borrower{'categorycode'} = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
2415
2416     my $sr = new String::Random;
2417     $sr->{'A'} = [ 'A'..'Z', 'a'..'z' ];
2418     my $password = $sr->randpattern("AAAAAAAAAA");
2419     $borrower{'password'} = $password;
2420
2421     $borrower{'cardnumber'} = fixup_cardnumber();
2422
2423     my $borrowernumber = AddMember(%borrower);
2424
2425     return ( $borrowernumber, $password );
2426 }
2427
2428 END { }    # module clean-up code here (global destructor)
2429
2430 1;
2431
2432 __END__
2433
2434 =head1 AUTHOR
2435
2436 Koha Team
2437
2438 =cut