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