Merge branch 'bug_7919' 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         &GetBorrowersWhoHaveNotBorrowedSince
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 GetBorrowersWhoHaveNotBorrowedSince
1942
1943   &GetBorrowersWhoHaveNotBorrowedSince($date)
1944
1945 this function get all borrowers who haven't borrowed since the date given on input arg.
1946
1947 =cut
1948
1949 sub GetBorrowersWhoHaveNotBorrowedSince {
1950     my $filterdate = shift||POSIX::strftime("%Y-%m-%d",localtime());
1951     my $filterexpiry = shift;
1952     my $filterbranch = shift || 
1953                         ((C4::Context->preference('IndependantBranches') 
1954                              && C4::Context->userenv 
1955                              && C4::Context->userenv->{flags} % 2 !=1 
1956                              && C4::Context->userenv->{branch})
1957                          ? C4::Context->userenv->{branch}
1958                          : "");  
1959     my $dbh   = C4::Context->dbh;
1960     my $query = "
1961         SELECT borrowers.borrowernumber,
1962                max(old_issues.timestamp) as latestissue,
1963                max(issues.timestamp) as currentissue
1964         FROM   borrowers
1965         JOIN   categories USING (categorycode)
1966         LEFT JOIN old_issues USING (borrowernumber)
1967         LEFT JOIN issues USING (borrowernumber) 
1968         WHERE  category_type <> 'S'
1969         AND borrowernumber NOT IN (SELECT guarantorid FROM borrowers WHERE guarantorid IS NOT NULL AND guarantorid <> 0) 
1970    ";
1971     my @query_params;
1972     if ($filterbranch && $filterbranch ne ""){ 
1973         $query.=" AND borrowers.branchcode= ?";
1974         push @query_params,$filterbranch;
1975     }
1976     if($filterexpiry){
1977         $query .= " AND dateexpiry < ? ";
1978         push @query_params,$filterdate;
1979     }
1980     $query.=" GROUP BY borrowers.borrowernumber";
1981     if ($filterdate){ 
1982         $query.=" HAVING (latestissue < ? OR latestissue IS NULL) 
1983                   AND currentissue IS NULL";
1984         push @query_params,$filterdate;
1985     }
1986     warn $query if $debug;
1987     my $sth = $dbh->prepare($query);
1988     if (scalar(@query_params)>0){  
1989         $sth->execute(@query_params);
1990     } 
1991     else {
1992         $sth->execute;
1993     }      
1994     
1995     my @results;
1996     while ( my $data = $sth->fetchrow_hashref ) {
1997         push @results, $data;
1998     }
1999     return \@results;
2000 }
2001
2002 =head2 GetBorrowersWhoHaveNeverBorrowed
2003
2004   $results = &GetBorrowersWhoHaveNeverBorrowed
2005
2006 This function get all borrowers who have never borrowed.
2007
2008 I<$result> is a ref to an array which all elements are a hasref.
2009
2010 =cut
2011
2012 sub GetBorrowersWhoHaveNeverBorrowed {
2013     my $filterbranch = shift || 
2014                         ((C4::Context->preference('IndependantBranches') 
2015                              && C4::Context->userenv 
2016                              && C4::Context->userenv->{flags} % 2 !=1 
2017                              && C4::Context->userenv->{branch})
2018                          ? C4::Context->userenv->{branch}
2019                          : "");  
2020     my $dbh   = C4::Context->dbh;
2021     my $query = "
2022         SELECT borrowers.borrowernumber,max(timestamp) as latestissue
2023         FROM   borrowers
2024           LEFT JOIN issues ON borrowers.borrowernumber = issues.borrowernumber
2025         WHERE issues.borrowernumber IS NULL
2026    ";
2027     my @query_params;
2028     if ($filterbranch && $filterbranch ne ""){ 
2029         $query.=" AND borrowers.branchcode= ?";
2030         push @query_params,$filterbranch;
2031     }
2032     warn $query if $debug;
2033   
2034     my $sth = $dbh->prepare($query);
2035     if (scalar(@query_params)>0){  
2036         $sth->execute(@query_params);
2037     } 
2038     else {
2039         $sth->execute;
2040     }      
2041     
2042     my @results;
2043     while ( my $data = $sth->fetchrow_hashref ) {
2044         push @results, $data;
2045     }
2046     return \@results;
2047 }
2048
2049 =head2 GetBorrowersWithIssuesHistoryOlderThan
2050
2051   $results = &GetBorrowersWithIssuesHistoryOlderThan($date)
2052
2053 this function get all borrowers who has an issue history older than I<$date> given on input arg.
2054
2055 I<$result> is a ref to an array which all elements are a hashref.
2056 This hashref is containt the number of time this borrowers has borrowed before I<$date> and the borrowernumber.
2057
2058 =cut
2059
2060 sub GetBorrowersWithIssuesHistoryOlderThan {
2061     my $dbh  = C4::Context->dbh;
2062     my $date = shift ||POSIX::strftime("%Y-%m-%d",localtime());
2063     my $filterbranch = shift || 
2064                         ((C4::Context->preference('IndependantBranches') 
2065                              && C4::Context->userenv 
2066                              && C4::Context->userenv->{flags} % 2 !=1 
2067                              && C4::Context->userenv->{branch})
2068                          ? C4::Context->userenv->{branch}
2069                          : "");  
2070     my $query = "
2071        SELECT count(borrowernumber) as n,borrowernumber
2072        FROM old_issues
2073        WHERE returndate < ?
2074          AND borrowernumber IS NOT NULL 
2075     "; 
2076     my @query_params;
2077     push @query_params, $date;
2078     if ($filterbranch){
2079         $query.="   AND branchcode = ?";
2080         push @query_params, $filterbranch;
2081     }    
2082     $query.=" GROUP BY borrowernumber ";
2083     warn $query if $debug;
2084     my $sth = $dbh->prepare($query);
2085     $sth->execute(@query_params);
2086     my @results;
2087
2088     while ( my $data = $sth->fetchrow_hashref ) {
2089         push @results, $data;
2090     }
2091     return \@results;
2092 }
2093
2094 =head2 GetBorrowersNamesAndLatestIssue
2095
2096   $results = &GetBorrowersNamesAndLatestIssueList(@borrowernumbers)
2097
2098 this function get borrowers Names and surnames and Issue information.
2099
2100 I<@borrowernumbers> is an array which all elements are borrowernumbers.
2101 This hashref is containt the number of time this borrowers has borrowed before I<$date> and the borrowernumber.
2102
2103 =cut
2104
2105 sub GetBorrowersNamesAndLatestIssue {
2106     my $dbh  = C4::Context->dbh;
2107     my @borrowernumbers=@_;  
2108     my $query = "
2109        SELECT surname,lastname, phone, email,max(timestamp)
2110        FROM borrowers 
2111          LEFT JOIN issues ON borrowers.borrowernumber=issues.borrowernumber
2112        GROUP BY borrowernumber
2113    ";
2114     my $sth = $dbh->prepare($query);
2115     $sth->execute;
2116     my $results = $sth->fetchall_arrayref({});
2117     return $results;
2118 }
2119
2120 =head2 DebarMember
2121
2122 my $success = DebarMember( $borrowernumber, $todate );
2123
2124 marks a Member as debarred, and therefore unable to checkout any more
2125 items.
2126
2127 return :
2128 true on success, false on failure
2129
2130 =cut
2131
2132 sub DebarMember {
2133     my $borrowernumber = shift;
2134     my $todate         = shift;
2135
2136     return unless defined $borrowernumber;
2137     return unless $borrowernumber =~ /^\d+$/;
2138
2139     return ModMember(
2140         borrowernumber => $borrowernumber,
2141         debarred       => $todate
2142     );
2143
2144 }
2145
2146 =head2 ModPrivacy
2147
2148 =over 4
2149
2150 my $success = ModPrivacy( $borrowernumber, $privacy );
2151
2152 Update the privacy of a patron.
2153
2154 return :
2155 true on success, false on failure
2156
2157 =back
2158
2159 =cut
2160
2161 sub ModPrivacy {
2162     my $borrowernumber = shift;
2163     my $privacy = shift;
2164     return unless defined $borrowernumber;
2165     return unless $borrowernumber =~ /^\d+$/;
2166
2167     return ModMember( borrowernumber => $borrowernumber,
2168                       privacy        => $privacy );
2169 }
2170
2171 =head2 AddMessage
2172
2173   AddMessage( $borrowernumber, $message_type, $message, $branchcode );
2174
2175 Adds a message to the messages table for the given borrower.
2176
2177 Returns:
2178   True on success
2179   False on failure
2180
2181 =cut
2182
2183 sub AddMessage {
2184     my ( $borrowernumber, $message_type, $message, $branchcode ) = @_;
2185
2186     my $dbh  = C4::Context->dbh;
2187
2188     if ( ! ( $borrowernumber && $message_type && $message && $branchcode ) ) {
2189       return;
2190     }
2191
2192     my $query = "INSERT INTO messages ( borrowernumber, branchcode, message_type, message ) VALUES ( ?, ?, ?, ? )";
2193     my $sth = $dbh->prepare($query);
2194     $sth->execute( $borrowernumber, $branchcode, $message_type, $message );
2195     logaction("MEMBERS", "ADDCIRCMESSAGE", $borrowernumber, $message) if C4::Context->preference("BorrowersLog");
2196     return 1;
2197 }
2198
2199 =head2 GetMessages
2200
2201   GetMessages( $borrowernumber, $type );
2202
2203 $type is message type, B for borrower, or L for Librarian.
2204 Empty type returns all messages of any type.
2205
2206 Returns all messages for the given borrowernumber
2207
2208 =cut
2209
2210 sub GetMessages {
2211     my ( $borrowernumber, $type, $branchcode ) = @_;
2212
2213     if ( ! $type ) {
2214       $type = '%';
2215     }
2216
2217     my $dbh  = C4::Context->dbh;
2218
2219     my $query = "SELECT
2220                   branches.branchname,
2221                   messages.*,
2222                   message_date,
2223                   messages.branchcode LIKE '$branchcode' AS can_delete
2224                   FROM messages, branches
2225                   WHERE borrowernumber = ?
2226                   AND message_type LIKE ?
2227                   AND messages.branchcode = branches.branchcode
2228                   ORDER BY message_date DESC";
2229     my $sth = $dbh->prepare($query);
2230     $sth->execute( $borrowernumber, $type ) ;
2231     my @results;
2232
2233     while ( my $data = $sth->fetchrow_hashref ) {
2234         my $d = C4::Dates->new( $data->{message_date}, 'iso' );
2235         $data->{message_date_formatted} = $d->output;
2236         push @results, $data;
2237     }
2238     return \@results;
2239
2240 }
2241
2242 =head2 GetMessages
2243
2244   GetMessagesCount( $borrowernumber, $type );
2245
2246 $type is message type, B for borrower, or L for Librarian.
2247 Empty type returns all messages of any type.
2248
2249 Returns the number of messages for the given borrowernumber
2250
2251 =cut
2252
2253 sub GetMessagesCount {
2254     my ( $borrowernumber, $type, $branchcode ) = @_;
2255
2256     if ( ! $type ) {
2257       $type = '%';
2258     }
2259
2260     my $dbh  = C4::Context->dbh;
2261
2262     my $query = "SELECT COUNT(*) as MsgCount FROM messages WHERE borrowernumber = ? AND message_type LIKE ?";
2263     my $sth = $dbh->prepare($query);
2264     $sth->execute( $borrowernumber, $type ) ;
2265     my @results;
2266
2267     my $data = $sth->fetchrow_hashref;
2268     my $count = $data->{'MsgCount'};
2269
2270     return $count;
2271 }
2272
2273
2274
2275 =head2 DeleteMessage
2276
2277   DeleteMessage( $message_id );
2278
2279 =cut
2280
2281 sub DeleteMessage {
2282     my ( $message_id ) = @_;
2283
2284     my $dbh = C4::Context->dbh;
2285     my $query = "SELECT * FROM messages WHERE message_id = ?";
2286     my $sth = $dbh->prepare($query);
2287     $sth->execute( $message_id );
2288     my $message = $sth->fetchrow_hashref();
2289
2290     $query = "DELETE FROM messages WHERE message_id = ?";
2291     $sth = $dbh->prepare($query);
2292     $sth->execute( $message_id );
2293     logaction("MEMBERS", "DELCIRCMESSAGE", $message->{'borrowernumber'}, $message->{'message'}) if C4::Context->preference("BorrowersLog");
2294 }
2295
2296 =head2 IssueSlip
2297
2298   IssueSlip($branchcode, $borrowernumber, $quickslip)
2299
2300   Returns letter hash ( see C4::Letters::GetPreparedLetter )
2301
2302   $quickslip is boolean, to indicate whether we want a quick slip
2303
2304 =cut
2305
2306 sub IssueSlip {
2307     my ($branch, $borrowernumber, $quickslip) = @_;
2308
2309 #   return unless ( C4::Context->boolean_preference('printcirculationslips') );
2310
2311     my $now       = POSIX::strftime("%Y-%m-%d", localtime);
2312
2313     my $issueslist = GetPendingIssues($borrowernumber);
2314     foreach my $it (@$issueslist){
2315         if ((substr $it->{'issuedate'}, 0, 10) eq $now) {
2316             $it->{'now'} = 1;
2317         }
2318         elsif ((substr $it->{'date_due'}, 0, 10) le $now) {
2319             $it->{'overdue'} = 1;
2320         }
2321
2322         $it->{'date_due'}=format_date($it->{'date_due'});
2323     }
2324     my @issues = sort { $b->{'timestamp'} <=> $a->{'timestamp'} } @$issueslist;
2325
2326     my ($letter_code, %repeat);
2327     if ( $quickslip ) {
2328         $letter_code = 'ISSUEQSLIP';
2329         %repeat =  (
2330             'checkedout' => [ map {
2331                 'biblio' => $_,
2332                 'items'  => $_,
2333                 'issues' => $_,
2334             }, grep { $_->{'now'} } @issues ],
2335         );
2336     }
2337     else {
2338         $letter_code = 'ISSUESLIP';
2339         %repeat =  (
2340             'checkedout' => [ map {
2341                 'biblio' => $_,
2342                 'items'  => $_,
2343                 'issues' => $_,
2344             }, grep { !$_->{'overdue'} } @issues ],
2345
2346             'overdue' => [ map {
2347                 'biblio' => $_,
2348                 'items'  => $_,
2349                 'issues' => $_,
2350             }, grep { $_->{'overdue'} } @issues ],
2351
2352             'news' => [ map {
2353                 $_->{'timestamp'} = $_->{'newdate'};
2354                 { opac_news => $_ }
2355             } @{ GetNewsToDisplay("slip") } ],
2356         );
2357     }
2358
2359     return  C4::Letters::GetPreparedLetter (
2360         module => 'circulation',
2361         letter_code => $letter_code,
2362         branchcode => $branch,
2363         tables => {
2364             'branches'    => $branch,
2365             'borrowers'   => $borrowernumber,
2366         },
2367         repeat => \%repeat,
2368     );
2369 }
2370
2371 =head2 GetBorrowersWithEmail
2372
2373     ([$borrnum,$userid], ...) = GetBorrowersWithEmail('me@example.com');
2374
2375 This gets a list of users and their basic details from their email address.
2376 As it's possible for multiple user to have the same email address, it provides
2377 you with all of them. If there is no userid for the user, there will be an
2378 C<undef> there. An empty list will be returned if there are no matches.
2379
2380 =cut
2381
2382 sub GetBorrowersWithEmail {
2383     my $email = shift;
2384
2385     my $dbh = C4::Context->dbh;
2386
2387     my $query = "SELECT borrowernumber, userid FROM borrowers WHERE email=?";
2388     my $sth=$dbh->prepare($query);
2389     $sth->execute($email);
2390     my @result = ();
2391     while (my $ref = $sth->fetch) {
2392         push @result, $ref;
2393     }
2394     die "Failure searching for borrowers by email address: $sth->errstr" if $sth->err;
2395     return @result;
2396 }
2397
2398 sub AddMember_Opac {
2399     my ( %borrower ) = @_;
2400
2401     $borrower{'categorycode'} = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
2402
2403     my $sr = new String::Random;
2404     $sr->{'A'} = [ 'A'..'Z', 'a'..'z' ];
2405     my $password = $sr->randpattern("AAAAAAAAAA");
2406     $borrower{'password'} = $password;
2407
2408     $borrower{'cardnumber'} = fixup_cardnumber();
2409
2410     my $borrowernumber = AddMember(%borrower);
2411
2412     return ( $borrowernumber, $password );
2413 }
2414
2415 END { }    # module clean-up code here (global destructor)
2416
2417 1;
2418
2419 __END__
2420
2421 =head1 AUTHOR
2422
2423 Koha Team
2424
2425 =cut