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