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