Bug 17554: Koha::Patrons - Remove GetBorrowersWithEmail
[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 Scalar::Util qw( looks_like_number );
28 use Date::Calc qw/Today 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 Text::Unaccent qw( unac_string );
41 use Koha::AuthUtils qw(hash_password);
42 use Koha::Database;
43 use Koha::Holds;
44 use Koha::List::Patron;
45 use Koha::Patrons;
46 use Koha::Patron::Categories;
47 use Koha::Schema;
48
49 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
50
51 use Module::Load::Conditional qw( can_load );
52 if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
53    $debug && warn "Unable to load Koha::NorwegianPatronDB";
54 }
55
56
57 BEGIN {
58     $debug = $ENV{DEBUG} || 0;
59     require Exporter;
60     @ISA = qw(Exporter);
61     #Get data
62     push @EXPORT, qw(
63         &GetMember
64
65         &GetPendingIssues
66         &GetAllIssues
67
68         &GetFirstValidEmailAddress
69         &GetNoticeEmailAddress
70
71         &GetMemberAccountRecords
72         &GetBorNotifyAcctRecord
73
74         &GetBorrowersToExpunge
75
76         &IssueSlip
77
78         GetOverduesForPatron
79     );
80
81     #Modify data
82     push @EXPORT, qw(
83         &ModMember
84         &changepassword
85     );
86
87     #Insert data
88     push @EXPORT, qw(
89         &AddMember
90     &AddMember_Auto
91         &AddMember_Opac
92     );
93
94     #Check data
95     push @EXPORT, qw(
96         &checkuniquemember
97         &checkuserpassword
98         &Check_Userid
99         &Generate_Userid
100         &fixup_cardnumber
101         &checkcardnumber
102     );
103 }
104
105 =head1 NAME
106
107 C4::Members - Perl Module containing convenience functions for member handling
108
109 =head1 SYNOPSIS
110
111 use C4::Members;
112
113 =head1 DESCRIPTION
114
115 This module contains routines for adding, modifying and deleting members/patrons/borrowers 
116
117 =head1 FUNCTIONS
118
119 =head2 patronflags
120
121  $flags = &patronflags($patron);
122
123 This function is not exported.
124
125 The following will be set where applicable:
126  $flags->{CHARGES}->{amount}        Amount of debt
127  $flags->{CHARGES}->{noissues}      Set if debt amount >$5.00 (or syspref noissuescharge)
128  $flags->{CHARGES}->{message}       Message -- deprecated
129
130  $flags->{CREDITS}->{amount}        Amount of credit
131  $flags->{CREDITS}->{message}       Message -- deprecated
132
133  $flags->{  GNA  }                  Patron has no valid address
134  $flags->{  GNA  }->{noissues}      Set for each GNA
135  $flags->{  GNA  }->{message}       "Borrower has no valid address" -- deprecated
136
137  $flags->{ LOST  }                  Patron's card reported lost
138  $flags->{ LOST  }->{noissues}      Set for each LOST
139  $flags->{ LOST  }->{message}       Message -- deprecated
140
141  $flags->{DBARRED}                  Set if patron debarred, no access
142  $flags->{DBARRED}->{noissues}      Set for each DBARRED
143  $flags->{DBARRED}->{message}       Message -- deprecated
144
145  $flags->{ NOTES }
146  $flags->{ NOTES }->{message}       The note itself.  NOT deprecated
147
148  $flags->{ ODUES }                  Set if patron has overdue books.
149  $flags->{ ODUES }->{message}       "Yes"  -- deprecated
150  $flags->{ ODUES }->{itemlist}      ref-to-array: list of overdue books
151  $flags->{ ODUES }->{itemlisttext}  Text list of overdue items -- deprecated
152
153  $flags->{WAITING}                  Set if any of patron's reserves are available
154  $flags->{WAITING}->{message}       Message -- deprecated
155  $flags->{WAITING}->{itemlist}      ref-to-array: list of available items
156
157 =over 
158
159 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
160 overdue items. Its elements are references-to-hash, each describing an
161 overdue item. The keys are selected fields from the issues, biblio,
162 biblioitems, and items tables of the Koha database.
163
164 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
165 the overdue items, one per line.  Deprecated.
166
167 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
168 available items. Each element is a reference-to-hash whose keys are
169 fields from the reserves table of the Koha database.
170
171 =back
172
173 All the "message" fields that include language generated in this function are deprecated, 
174 because such strings belong properly in the display layer.
175
176 The "message" field that comes from the DB is OK.
177
178 =cut
179
180 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
181 # FIXME rename this function.
182 sub patronflags {
183     my %flags;
184     my ( $patroninformation) = @_;
185     my $dbh=C4::Context->dbh;
186     my ($balance, $owing) = GetMemberAccountBalance( $patroninformation->{'borrowernumber'});
187     if ( $owing > 0 ) {
188         my %flaginfo;
189         my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
190         $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
191         $flaginfo{'amount'}  = sprintf "%.02f", $owing;
192         if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
193             $flaginfo{'noissues'} = 1;
194         }
195         $flags{'CHARGES'} = \%flaginfo;
196     }
197     elsif ( $balance < 0 ) {
198         my %flaginfo;
199         $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
200         $flaginfo{'amount'}  = sprintf "%.02f", $balance;
201         $flags{'CREDITS'} = \%flaginfo;
202     }
203
204     # Check the debt of the guarntees of this patron
205     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
206     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
207     if ( defined $no_issues_charge_guarantees ) {
208         my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
209         my @guarantees = $p->guarantees();
210         my $guarantees_non_issues_charges;
211         foreach my $g ( @guarantees ) {
212             my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id );
213             $guarantees_non_issues_charges += $n;
214         }
215
216         if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
217             my %flaginfo;
218             $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
219             $flaginfo{'amount'}  = $guarantees_non_issues_charges;
220             $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
221             $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
222         }
223     }
224
225     if (   $patroninformation->{'gonenoaddress'}
226         && $patroninformation->{'gonenoaddress'} == 1 )
227     {
228         my %flaginfo;
229         $flaginfo{'message'}  = 'Borrower has no valid address.';
230         $flaginfo{'noissues'} = 1;
231         $flags{'GNA'}         = \%flaginfo;
232     }
233     if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
234         my %flaginfo;
235         $flaginfo{'message'}  = 'Borrower\'s card reported lost.';
236         $flaginfo{'noissues'} = 1;
237         $flags{'LOST'}        = \%flaginfo;
238     }
239     if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
240         if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
241             my %flaginfo;
242             $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
243             $flaginfo{'message'}         = $patroninformation->{'debarredcomment'};
244             $flaginfo{'noissues'}        = 1;
245             $flaginfo{'dateend'}         = $patroninformation->{'debarred'};
246             $flags{'DBARRED'}           = \%flaginfo;
247         }
248     }
249     if (   $patroninformation->{'borrowernotes'}
250         && $patroninformation->{'borrowernotes'} )
251     {
252         my %flaginfo;
253         $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
254         $flags{'NOTES'}      = \%flaginfo;
255     }
256     my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
257     if ( $odues && $odues > 0 ) {
258         my %flaginfo;
259         $flaginfo{'message'}  = "Yes";
260         $flaginfo{'itemlist'} = $itemsoverdue;
261         foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
262             @$itemsoverdue )
263         {
264             $flaginfo{'itemlisttext'} .=
265               "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n";  # newline is display layer
266         }
267         $flags{'ODUES'} = \%flaginfo;
268     }
269
270     my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
271     my $waiting_holds = $patron->holds->search({ found => 'W' });
272     my $nowaiting = $waiting_holds->count;
273     if ( $nowaiting > 0 ) {
274         my %flaginfo;
275         $flaginfo{'message'}  = "Reserved items available";
276         $flaginfo{'itemlist'} = $waiting_holds->unblessed;
277         $flags{'WAITING'}     = \%flaginfo;
278     }
279     return ( \%flags );
280 }
281
282
283 =head2 GetMember
284
285   $borrower = &GetMember(%information);
286
287 Retrieve the first patron record meeting on criteria listed in the
288 C<%information> hash, which should contain one or more
289 pairs of borrowers column names and values, e.g.,
290
291    $borrower = GetMember(borrowernumber => id);
292
293 C<&GetBorrower> returns a reference-to-hash whose keys are the fields of
294 the C<borrowers> table in the Koha database.
295
296 FIXME: GetMember() is used throughout the code as a lookup
297 on a unique key such as the borrowernumber, but this meaning is not
298 enforced in the routine itself.
299
300 =cut
301
302 #'
303 sub GetMember {
304     my ( %information ) = @_;
305     if (exists $information{borrowernumber} && !defined $information{borrowernumber}) {
306         #passing mysql's kohaadmin?? Makes no sense as a query
307         return;
308     }
309     my $dbh = C4::Context->dbh;
310     my $select =
311     q{SELECT borrowers.*, categories.category_type, categories.description
312     FROM borrowers 
313     LEFT JOIN categories on borrowers.categorycode=categories.categorycode WHERE };
314     my $more_p = 0;
315     my @values = ();
316     for (keys %information ) {
317         if ($more_p) {
318             $select .= ' AND ';
319         }
320         else {
321             $more_p++;
322         }
323
324         if (defined $information{$_}) {
325             $select .= "$_ = ?";
326             push @values, $information{$_};
327         }
328         else {
329             $select .= "$_ IS NULL";
330         }
331     }
332     $debug && warn $select, " ",values %information;
333     my $sth = $dbh->prepare("$select");
334     $sth->execute(@values);
335     my $data = $sth->fetchall_arrayref({});
336     #FIXME interface to this routine now allows generation of a result set
337     #so whole array should be returned but bowhere in the current code expects this
338     if (@{$data} ) {
339         return $data->[0];
340     }
341
342     return;
343 }
344
345 =head2 ModMember
346
347   my $success = ModMember(borrowernumber => $borrowernumber,
348                                             [ field => value ]... );
349
350 Modify borrower's data.  All date fields should ALREADY be in ISO format.
351
352 return :
353 true on success, or false on failure
354
355 =cut
356
357 sub ModMember {
358     my (%data) = @_;
359
360     # trim whitespace from data which has some non-whitespace in it.
361     foreach my $field_name (keys(%data)) {
362         if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
363             $data{$field_name} =~ s/^\s*|\s*$//g;
364         }
365     }
366
367     # test to know if you must update or not the borrower password
368     if (exists $data{password}) {
369         if ($data{password} eq '****' or $data{password} eq '') {
370             delete $data{password};
371         } else {
372             if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
373                 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
374                 Koha::NorwegianPatronDB::NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
375             }
376             $data{password} = hash_password($data{password});
377         }
378     }
379
380     my $old_categorycode = Koha::Patrons->find( $data{borrowernumber} )->categorycode;
381
382     # get only the columns of a borrower
383     my $schema = Koha::Database->new()->schema;
384     my @columns = $schema->source('Borrower')->columns;
385     my $new_borrower = { map { join(' ', @columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) };
386
387     $new_borrower->{dateofbirth}     ||= undef if exists $new_borrower->{dateofbirth};
388     $new_borrower->{dateenrolled}    ||= undef if exists $new_borrower->{dateenrolled};
389     $new_borrower->{dateexpiry}      ||= undef if exists $new_borrower->{dateexpiry};
390     $new_borrower->{debarred}        ||= undef if exists $new_borrower->{debarred};
391     $new_borrower->{sms_provider_id} ||= undef if exists $new_borrower->{sms_provider_id};
392     $new_borrower->{guarantorid}     ||= undef if exists $new_borrower->{guarantorid};
393
394     my $patron = Koha::Patrons->find( $new_borrower->{borrowernumber} );
395
396     delete $new_borrower->{userid} if exists $new_borrower->{userid} and not $new_borrower->{userid};
397
398     my $execute_success = $patron->store if $patron->set($new_borrower);
399
400     if ($execute_success) { # only proceed if the update was a success
401         # If the patron changes to a category with enrollment fee, we add a fee
402         if ( $data{categorycode} and $data{categorycode} ne $old_categorycode ) {
403             if ( C4::Context->preference('FeeOnChangePatronCategory') ) {
404                 $patron->add_enrolment_fee_if_needed;
405             }
406         }
407
408         # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
409         # cronjob will use for syncing with NL
410         if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
411             my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
412                 'synctype'       => 'norwegianpatrondb',
413                 'borrowernumber' => $data{'borrowernumber'}
414             });
415             # Do not set to "edited" if syncstatus is "new". We need to sync as new before
416             # we can sync as changed. And the "new sync" will pick up all changes since
417             # the patron was created anyway.
418             if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
419                 $borrowersync->update( { 'syncstatus' => 'edited' } );
420             }
421             # Set the value of 'sync'
422             $borrowersync->update( { 'sync' => $data{'sync'} } );
423             # Try to do the live sync
424             Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
425         }
426
427         logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog");
428     }
429     return $execute_success;
430 }
431
432 =head2 AddMember
433
434   $borrowernumber = &AddMember(%borrower);
435
436 insert new borrower into table
437
438 (%borrower keys are database columns. Database columns could be
439 different in different versions. Please look into database for correct
440 column names.)
441
442 Returns the borrowernumber upon success
443
444 Returns as undef upon any db error without further processing
445
446 =cut
447
448 #'
449 sub AddMember {
450     my (%data) = @_;
451     my $dbh = C4::Context->dbh;
452     my $schema = Koha::Database->new()->schema;
453
454     # trim whitespace from data which has some non-whitespace in it.
455     foreach my $field_name (keys(%data)) {
456         if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
457             $data{$field_name} =~ s/^\s*|\s*$//g;
458         }
459     }
460
461     # generate a proper login if none provided
462     $data{'userid'} = Generate_Userid( $data{'borrowernumber'}, $data{'firstname'}, $data{'surname'} )
463       if ( $data{'userid'} eq '' || !Check_Userid( $data{'userid'} ) );
464
465     # add expiration date if it isn't already there
466     $data{dateexpiry} ||= Koha::Patron::Categories->find( $data{categorycode} )->get_expiry_date;
467
468     # add enrollment date if it isn't already there
469     unless ( $data{'dateenrolled'} ) {
470         $data{'dateenrolled'} = output_pref( { dt => dt_from_string, dateonly => 1, dateformat => 'iso' } );
471     }
472
473     if ( C4::Context->preference("autoMemberNum") ) {
474         if ( not exists $data{cardnumber} or not defined $data{cardnumber} or $data{cardnumber} eq '' ) {
475             $data{cardnumber} = fixup_cardnumber( $data{cardnumber} );
476         }
477     }
478
479     my $patron_category = $schema->resultset('Category')->find( $data{'categorycode'} );
480     $data{'privacy'} =
481         $patron_category->default_privacy() eq 'default' ? 1
482       : $patron_category->default_privacy() eq 'never'   ? 2
483       : $patron_category->default_privacy() eq 'forever' ? 0
484       :                                                    undef;
485
486     $data{'privacy_guarantor_checkouts'} = 0 unless defined( $data{'privacy_guarantor_checkouts'} );
487
488     # Make a copy of the plain text password for later use
489     my $plain_text_password = $data{'password'};
490
491     # create a disabled account if no password provided
492     $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!';
493
494     # we don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00
495     $data{'dateofbirth'}     = undef if ( not $data{'dateofbirth'} );
496     $data{'debarred'}        = undef if ( not $data{'debarred'} );
497     $data{'sms_provider_id'} = undef if ( not $data{'sms_provider_id'} );
498     $data{'guarantorid'}     = undef if ( not $data{'guarantorid'} );
499
500     # get only the columns of Borrower
501     # FIXME Do we really need this check?
502     my @columns = $schema->source('Borrower')->columns;
503     my $new_member = { map { join(' ',@columns) =~ /$_/ ? ( $_ => $data{$_} )  : () } keys(%data) } ;
504
505     delete $new_member->{borrowernumber};
506
507     my $patron = Koha::Patron->new( $new_member )->store;
508     $data{borrowernumber} = $patron->borrowernumber;
509
510     # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
511     # cronjob will use for syncing with NL
512     if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
513         Koha::Database->new->schema->resultset('BorrowerSync')->create({
514             'borrowernumber' => $data{'borrowernumber'},
515             'synctype'       => 'norwegianpatrondb',
516             'sync'           => 1,
517             'syncstatus'     => 'new',
518             'hashed_pin'     => Koha::NorwegianPatronDB::NLEncryptPIN( $plain_text_password ),
519         });
520     }
521
522     logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog");
523
524     $patron->add_enrolment_fee_if_needed;
525
526     return $data{borrowernumber};
527 }
528
529 =head2 Check_Userid
530
531     my $uniqueness = Check_Userid($userid,$borrowernumber);
532
533     $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 != '').
534
535     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.
536
537     return :
538         0 for not unique (i.e. this $userid already exists)
539         1 for unique (i.e. this $userid does not exist, or this $userid/$borrowernumber combination already exists)
540
541 =cut
542
543 sub Check_Userid {
544     my ( $uid, $borrowernumber ) = @_;
545
546     return 0 unless ($uid); # userid is a unique column, we should assume NULL is not unique
547
548     return 0 if ( $uid eq C4::Context->config('user') );
549
550     my $rs = Koha::Database->new()->schema()->resultset('Borrower');
551
552     my $params;
553     $params->{userid} = $uid;
554     $params->{borrowernumber} = { '!=' => $borrowernumber } if ($borrowernumber);
555
556     my $count = $rs->count( $params );
557
558     return $count ? 0 : 1;
559 }
560
561 =head2 Generate_Userid
562
563     my $newuid = Generate_Userid($borrowernumber, $firstname, $surname);
564
565     Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
566
567     $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.
568
569     return :
570         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).
571
572 =cut
573
574 sub Generate_Userid {
575   my ($borrowernumber, $firstname, $surname) = @_;
576   my $newuid;
577   my $offset = 0;
578   #The script will "do" the following code and increment the $offset until Check_Userid = 1 (i.e. until $newuid comes back as unique)
579   do {
580     $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
581     $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
582     $newuid = lc(($firstname)? "$firstname.$surname" : $surname);
583     $newuid = unac_string('utf-8',$newuid);
584     $newuid .= $offset unless $offset == 0;
585     $offset++;
586
587    } while (!Check_Userid($newuid,$borrowernumber));
588
589    return $newuid;
590 }
591
592 =head2 fixup_cardnumber
593
594 Warning: The caller is responsible for locking the members table in write
595 mode, to avoid database corruption.
596
597 =cut
598
599 use vars qw( @weightings );
600 my @weightings = ( 8, 4, 6, 3, 5, 2, 1 );
601
602 sub fixup_cardnumber {
603     my ($cardnumber) = @_;
604     my $autonumber_members = C4::Context->boolean_preference('autoMemberNum') || 0;
605
606     # Find out whether member numbers should be generated
607     # automatically. Should be either "1" or something else.
608     # Defaults to "0", which is interpreted as "no".
609
610     #     if ($cardnumber !~ /\S/ && $autonumber_members) {
611     ($autonumber_members) or return $cardnumber;
612     my $checkdigit = C4::Context->preference('checkdigit');
613     my $dbh = C4::Context->dbh;
614     if ( $checkdigit and $checkdigit eq 'katipo' ) {
615
616         # if checkdigit is selected, calculate katipo-style cardnumber.
617         # otherwise, just use the max()
618         # purpose: generate checksum'd member numbers.
619         # We'll assume we just got the max value of digits 2-8 of member #'s
620         # from the database and our job is to increment that by one,
621         # determine the 1st and 9th digits and return the full string.
622         my $sth = $dbh->prepare(
623             "select max(substring(borrowers.cardnumber,2,7)) as new_num from borrowers"
624         );
625         $sth->execute;
626         my $data = $sth->fetchrow_hashref;
627         $cardnumber = $data->{new_num};
628         if ( !$cardnumber ) {    # If DB has no values,
629             $cardnumber = 1000000;    # start at 1000000
630         } else {
631             $cardnumber += 1;
632         }
633
634         my $sum = 0;
635         for ( my $i = 0 ; $i < 8 ; $i += 1 ) {
636             # read weightings, left to right, 1 char at a time
637             my $temp1 = $weightings[$i];
638
639             # sequence left to right, 1 char at a time
640             my $temp2 = substr( $cardnumber, $i, 1 );
641
642             # mult each char 1-7 by its corresponding weighting
643             $sum += $temp1 * $temp2;
644         }
645
646         my $rem = ( $sum % 11 );
647         $rem = 'X' if $rem == 10;
648
649         return "V$cardnumber$rem";
650      } else {
651
652         my $sth = $dbh->prepare(
653             'SELECT MAX( CAST( cardnumber AS SIGNED ) ) FROM borrowers WHERE cardnumber REGEXP "^-?[0-9]+$"'
654         );
655         $sth->execute;
656         my ($result) = $sth->fetchrow;
657         return $result + 1;
658     }
659     return $cardnumber;     # just here as a fallback/reminder 
660 }
661
662 =head2 GetPendingIssues
663
664   my $issues = &GetPendingIssues(@borrowernumber);
665
666 Looks up what the patron with the given borrowernumber has borrowed.
667
668 C<&GetPendingIssues> returns a
669 reference-to-array where each element is a reference-to-hash; the
670 keys are the fields from the C<issues>, C<biblio>, and C<items> tables.
671 The keys include C<biblioitems> fields.
672
673 =cut
674
675 sub GetPendingIssues {
676     my @borrowernumbers = @_;
677
678     unless (@borrowernumbers ) { # return a ref_to_array
679         return \@borrowernumbers; # to not cause surprise to caller
680     }
681
682     # Borrowers part of the query
683     my $bquery = '';
684     for (my $i = 0; $i < @borrowernumbers; $i++) {
685         $bquery .= ' issues.borrowernumber = ?';
686         if ($i < $#borrowernumbers ) {
687             $bquery .= ' OR';
688         }
689     }
690
691     # FIXME: namespace collision: each table has "timestamp" fields.  Which one is "timestamp" ?
692     # FIXME: circ/ciculation.pl tries to sort by timestamp!
693     # FIXME: namespace collision: other collisions possible.
694     # FIXME: most of this data isn't really being used by callers.
695     my $query =
696    "SELECT issues.*,
697             items.*,
698            biblio.*,
699            biblioitems.volume,
700            biblioitems.number,
701            biblioitems.itemtype,
702            biblioitems.isbn,
703            biblioitems.issn,
704            biblioitems.publicationyear,
705            biblioitems.publishercode,
706            biblioitems.volumedate,
707            biblioitems.volumedesc,
708            biblioitems.lccn,
709            biblioitems.url,
710            borrowers.firstname,
711            borrowers.surname,
712            borrowers.cardnumber,
713            issues.timestamp AS timestamp,
714            issues.renewals  AS renewals,
715            issues.borrowernumber AS borrowernumber,
716             items.renewals  AS totalrenewals
717     FROM   issues
718     LEFT JOIN items       ON items.itemnumber       =      issues.itemnumber
719     LEFT JOIN biblio      ON items.biblionumber     =      biblio.biblionumber
720     LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
721     LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
722     WHERE
723       $bquery
724     ORDER BY issues.issuedate"
725     ;
726
727     my $sth = C4::Context->dbh->prepare($query);
728     $sth->execute(@borrowernumbers);
729     my $data = $sth->fetchall_arrayref({});
730     my $today = dt_from_string;
731     foreach (@{$data}) {
732         if ($_->{issuedate}) {
733             $_->{issuedate} = dt_from_string($_->{issuedate}, 'sql');
734         }
735         $_->{date_due_sql} = $_->{date_due};
736         # FIXME no need to have this value
737         $_->{date_due} or next;
738         $_->{date_due_sql} = $_->{date_due};
739         # FIXME no need to have this value
740         $_->{date_due} = dt_from_string($_->{date_due}, 'sql');
741         if ( DateTime->compare($_->{date_due}, $today) == -1 ) {
742             $_->{overdue} = 1;
743         }
744     }
745     return $data;
746 }
747
748 =head2 GetAllIssues
749
750   $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
751
752 Looks up what the patron with the given borrowernumber has borrowed,
753 and sorts the results.
754
755 C<$sortkey> is the name of a field on which to sort the results. This
756 should be the name of a field in the C<issues>, C<biblio>,
757 C<biblioitems>, or C<items> table in the Koha database.
758
759 C<$limit> is the maximum number of results to return.
760
761 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
762 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
763 C<items> tables of the Koha database.
764
765 =cut
766
767 #'
768 sub GetAllIssues {
769     my ( $borrowernumber, $order, $limit ) = @_;
770
771     return unless $borrowernumber;
772     $order = 'date_due desc' unless $order;
773
774     my $dbh = C4::Context->dbh;
775     my $query =
776 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
777   FROM issues 
778   LEFT JOIN items on items.itemnumber=issues.itemnumber
779   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
780   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
781   WHERE borrowernumber=? 
782   UNION ALL
783   SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp 
784   FROM old_issues 
785   LEFT JOIN items on items.itemnumber=old_issues.itemnumber
786   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
787   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
788   WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
789   order by ' . $order;
790     if ($limit) {
791         $query .= " limit $limit";
792     }
793
794     my $sth = $dbh->prepare($query);
795     $sth->execute( $borrowernumber, $borrowernumber );
796     return $sth->fetchall_arrayref( {} );
797 }
798
799
800 =head2 GetMemberAccountRecords
801
802   ($total, $acctlines, $count) = &GetMemberAccountRecords($borrowernumber);
803
804 Looks up accounting data for the patron with the given borrowernumber.
805
806 C<&GetMemberAccountRecords> returns a three-element array. C<$acctlines> is a
807 reference-to-array, where each element is a reference-to-hash; the
808 keys are the fields of the C<accountlines> table in the Koha database.
809 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
810 total amount outstanding for all of the account lines.
811
812 =cut
813
814 sub GetMemberAccountRecords {
815     my ($borrowernumber) = @_;
816     my $dbh = C4::Context->dbh;
817     my @acctlines;
818     my $numlines = 0;
819     my $strsth      = qq(
820                         SELECT * 
821                         FROM accountlines 
822                         WHERE borrowernumber=?);
823     $strsth.=" ORDER BY accountlines_id desc";
824     my $sth= $dbh->prepare( $strsth );
825     $sth->execute( $borrowernumber );
826
827     my $total = 0;
828     while ( my $data = $sth->fetchrow_hashref ) {
829         if ( $data->{itemnumber} ) {
830             my $biblio = GetBiblioFromItemNumber( $data->{itemnumber} );
831             $data->{biblionumber} = $biblio->{biblionumber};
832             $data->{title}        = $biblio->{title};
833         }
834         $acctlines[$numlines] = $data;
835         $numlines++;
836         $total += sprintf "%.0f", 1000*$data->{amountoutstanding}; # convert float to integer to avoid round-off errors
837     }
838     $total /= 1000;
839     return ( $total, \@acctlines,$numlines);
840 }
841
842 =head2 GetMemberAccountBalance
843
844   ($total_balance, $non_issue_balance, $other_charges) = &GetMemberAccountBalance($borrowernumber);
845
846 Calculates amount immediately owing by the patron - non-issue charges.
847 Based on GetMemberAccountRecords.
848 Charges exempt from non-issue are:
849 * Res (reserves)
850 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
851 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
852
853 =cut
854
855 sub GetMemberAccountBalance {
856     my ($borrowernumber) = @_;
857
858     my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
859
860     my @not_fines;
861     push @not_fines, 'Res' unless C4::Context->preference('HoldsInNoissuesCharge');
862     push @not_fines, 'Rent' unless C4::Context->preference('RentalsInNoissuesCharge');
863     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
864         my $dbh = C4::Context->dbh;
865         my $man_inv_types = $dbh->selectcol_arrayref(qq{SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'});
866         push @not_fines, map substr($_, 0, $ACCOUNT_TYPE_LENGTH), @$man_inv_types;
867     }
868     my %not_fine = map {$_ => 1} @not_fines;
869
870     my ($total, $acctlines) = GetMemberAccountRecords($borrowernumber);
871     my $other_charges = 0;
872     foreach (@$acctlines) {
873         $other_charges += $_->{amountoutstanding} if $not_fine{ substr($_->{accounttype}, 0, $ACCOUNT_TYPE_LENGTH) };
874     }
875
876     return ( $total, $total - $other_charges, $other_charges);
877 }
878
879 =head2 GetBorNotifyAcctRecord
880
881   ($total, $acctlines, $count) = &GetBorNotifyAcctRecord($params,$notifyid);
882
883 Looks up accounting data for the patron with the given borrowernumber per file number.
884
885 C<&GetBorNotifyAcctRecord> returns a three-element array. C<$acctlines> is a
886 reference-to-array, where each element is a reference-to-hash; the
887 keys are the fields of the C<accountlines> table in the Koha database.
888 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
889 total amount outstanding for all of the account lines.
890
891 =cut
892
893 sub GetBorNotifyAcctRecord {
894     my ( $borrowernumber, $notifyid ) = @_;
895     my $dbh = C4::Context->dbh;
896     my @acctlines;
897     my $numlines = 0;
898     my $sth = $dbh->prepare(
899             "SELECT * 
900                 FROM accountlines 
901                 WHERE borrowernumber=? 
902                     AND notify_id=? 
903                     AND amountoutstanding != '0' 
904                 ORDER BY notify_id,accounttype
905                 ");
906
907     $sth->execute( $borrowernumber, $notifyid );
908     my $total = 0;
909     while ( my $data = $sth->fetchrow_hashref ) {
910         if ( $data->{itemnumber} ) {
911             my $biblio = GetBiblioFromItemNumber( $data->{itemnumber} );
912             $data->{biblionumber} = $biblio->{biblionumber};
913             $data->{title}        = $biblio->{title};
914         }
915         $acctlines[$numlines] = $data;
916         $numlines++;
917         $total += int(100 * $data->{'amountoutstanding'});
918     }
919     $total /= 100;
920     return ( $total, \@acctlines, $numlines );
921 }
922
923 sub checkcardnumber {
924     my ( $cardnumber, $borrowernumber ) = @_;
925
926     # If cardnumber is null, we assume they're allowed.
927     return 0 unless defined $cardnumber;
928
929     my $dbh = C4::Context->dbh;
930     my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
931     $query .= " AND borrowernumber <> ?" if ($borrowernumber);
932     my $sth = $dbh->prepare($query);
933     $sth->execute(
934         $cardnumber,
935         ( $borrowernumber ? $borrowernumber : () )
936     );
937
938     return 1 if $sth->fetchrow_hashref;
939
940     my ( $min_length, $max_length ) = get_cardnumber_length();
941     return 2
942         if length $cardnumber > $max_length
943         or length $cardnumber < $min_length;
944
945     return 0;
946 }
947
948 =head2 get_cardnumber_length
949
950     my ($min, $max) = C4::Members::get_cardnumber_length()
951
952 Returns the minimum and maximum length for patron cardnumbers as
953 determined by the CardnumberLength system preference, the
954 BorrowerMandatoryField system preference, and the width of the
955 database column.
956
957 =cut
958
959 sub get_cardnumber_length {
960     my ( $min, $max ) = ( 0, 16 ); # borrowers.cardnumber is a nullable varchar(16)
961     $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
962     if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
963         # Is integer and length match
964         if ( $cardnumber_length =~ m|^\d+$| ) {
965             $min = $max = $cardnumber_length
966                 if $cardnumber_length >= $min
967                     and $cardnumber_length <= $max;
968         }
969         # Else assuming it is a range
970         elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
971             $min = $1 if $1 and $min < $1;
972             $max = $2 if $2 and $max > $2;
973         }
974
975     }
976     my $borrower = Koha::Schema->resultset('Borrower');
977     my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
978     $min = $field_size if $min > $field_size;
979     return ( $min, $max );
980 }
981
982 =head2 GetFirstValidEmailAddress
983
984   $email = GetFirstValidEmailAddress($borrowernumber);
985
986 Return the first valid email address for a borrower, given the borrowernumber.  For now, the order 
987 is defined as email, emailpro, B_email.  Returns the empty string if the borrower has no email 
988 addresses.
989
990 =cut
991
992 sub GetFirstValidEmailAddress {
993     my $borrowernumber = shift;
994
995     my $borrower = Koha::Patrons->find( $borrowernumber );
996
997     return $borrower->first_valid_email_address();
998 }
999
1000 =head2 GetNoticeEmailAddress
1001
1002   $email = GetNoticeEmailAddress($borrowernumber);
1003
1004 Return the email address of borrower used for notices, given the borrowernumber.
1005 Returns the empty string if no email address.
1006
1007 =cut
1008
1009 sub GetNoticeEmailAddress {
1010     my $borrowernumber = shift;
1011
1012     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1013     # if syspref is set to 'first valid' (value == OFF), look up email address
1014     if ( $which_address eq 'OFF' ) {
1015         return GetFirstValidEmailAddress($borrowernumber);
1016     }
1017     # specified email address field
1018     my $dbh = C4::Context->dbh;
1019     my $sth = $dbh->prepare( qq{
1020         SELECT $which_address AS primaryemail
1021         FROM borrowers
1022         WHERE borrowernumber=?
1023     } );
1024     $sth->execute($borrowernumber);
1025     my $data = $sth->fetchrow_hashref;
1026     return $data->{'primaryemail'} || '';
1027 }
1028
1029 =head2 GetBorrowersToExpunge
1030
1031   $borrowers = &GetBorrowersToExpunge(
1032       not_borrowed_since => $not_borrowed_since,
1033       expired_before       => $expired_before,
1034       category_code        => $category_code,
1035       patron_list_id       => $patron_list_id,
1036       branchcode           => $branchcode
1037   );
1038
1039   This function get all borrowers based on the given criteria.
1040
1041 =cut
1042
1043 sub GetBorrowersToExpunge {
1044
1045     my $params = shift;
1046     my $filterdate       = $params->{'not_borrowed_since'};
1047     my $filterexpiry     = $params->{'expired_before'};
1048     my $filterlastseen   = $params->{'last_seen'};
1049     my $filtercategory   = $params->{'category_code'};
1050     my $filterbranch     = $params->{'branchcode'} ||
1051                         ((C4::Context->preference('IndependentBranches')
1052                              && C4::Context->userenv 
1053                              && !C4::Context->IsSuperLibrarian()
1054                              && C4::Context->userenv->{branch})
1055                          ? C4::Context->userenv->{branch}
1056                          : "");  
1057     my $filterpatronlist = $params->{'patron_list_id'};
1058
1059     my $dbh   = C4::Context->dbh;
1060     my $query = q|
1061         SELECT borrowers.borrowernumber,
1062                MAX(old_issues.timestamp) AS latestissue,
1063                MAX(issues.timestamp) AS currentissue
1064         FROM   borrowers
1065         JOIN   categories USING (categorycode)
1066         LEFT JOIN (
1067             SELECT guarantorid
1068             FROM borrowers
1069             WHERE guarantorid IS NOT NULL
1070                 AND guarantorid <> 0
1071         ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
1072         LEFT JOIN old_issues USING (borrowernumber)
1073         LEFT JOIN issues USING (borrowernumber)|;
1074     if ( $filterpatronlist  ){
1075         $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
1076     }
1077     $query .= q| WHERE  category_type <> 'S'
1078         AND tmp.guarantorid IS NULL
1079    |;
1080     my @query_params;
1081     if ( $filterbranch && $filterbranch ne "" ) {
1082         $query.= " AND borrowers.branchcode = ? ";
1083         push( @query_params, $filterbranch );
1084     }
1085     if ( $filterexpiry ) {
1086         $query .= " AND dateexpiry < ? ";
1087         push( @query_params, $filterexpiry );
1088     }
1089     if ( $filterlastseen ) {
1090         $query .= ' AND lastseen < ? ';
1091         push @query_params, $filterlastseen;
1092     }
1093     if ( $filtercategory ) {
1094         $query .= " AND categorycode = ? ";
1095         push( @query_params, $filtercategory );
1096     }
1097     if ( $filterpatronlist ){
1098         $query.=" AND patron_list_id = ? ";
1099         push( @query_params, $filterpatronlist );
1100     }
1101     $query.=" GROUP BY borrowers.borrowernumber HAVING currentissue IS NULL ";
1102     if ( $filterdate ) {
1103         $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
1104         push @query_params,$filterdate;
1105     }
1106     warn $query if $debug;
1107
1108     my $sth = $dbh->prepare($query);
1109     if (scalar(@query_params)>0){  
1110         $sth->execute(@query_params);
1111     }
1112     else {
1113         $sth->execute;
1114     }
1115     
1116     my @results;
1117     while ( my $data = $sth->fetchrow_hashref ) {
1118         push @results, $data;
1119     }
1120     return \@results;
1121 }
1122
1123 =head2 IssueSlip
1124
1125   IssueSlip($branchcode, $borrowernumber, $quickslip)
1126
1127   Returns letter hash ( see C4::Letters::GetPreparedLetter )
1128
1129   $quickslip is boolean, to indicate whether we want a quick slip
1130
1131   IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
1132
1133   Both slips:
1134
1135       <<branches.*>>
1136       <<borrowers.*>>
1137
1138   ISSUESLIP:
1139
1140       <checkedout>
1141          <<biblio.*>>
1142          <<items.*>>
1143          <<biblioitems.*>>
1144          <<issues.*>>
1145       </checkedout>
1146
1147       <overdue>
1148          <<biblio.*>>
1149          <<items.*>>
1150          <<biblioitems.*>>
1151          <<issues.*>>
1152       </overdue>
1153
1154       <news>
1155          <<opac_news.*>>
1156       </news>
1157
1158   ISSUEQSLIP:
1159
1160       <checkedout>
1161          <<biblio.*>>
1162          <<items.*>>
1163          <<biblioitems.*>>
1164          <<issues.*>>
1165       </checkedout>
1166
1167   NOTE: Not all table fields are available, pleasee see GetPendingIssues for a list of available fields.
1168
1169 =cut
1170
1171 sub IssueSlip {
1172     my ($branch, $borrowernumber, $quickslip) = @_;
1173
1174     # FIXME Check callers before removing this statement
1175     #return unless $borrowernumber;
1176
1177     my $patron = Koha::Patrons->find( $borrowernumber );
1178     return unless $patron;
1179
1180     my @issues = @{ GetPendingIssues($borrowernumber) };
1181
1182     for my $issue (@issues) {
1183         $issue->{date_due} = $issue->{date_due_sql};
1184         if ($quickslip) {
1185             my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1186             if ( substr( $issue->{issuedate}, 0, 10 ) eq $today
1187                 or substr( $issue->{lastreneweddate}, 0, 10 ) eq $today ) {
1188                   $issue->{now} = 1;
1189             };
1190         }
1191     }
1192
1193     # Sort on timestamp then on issuedate (useful for tests and could be if modified in a batch
1194     @issues = sort {
1195         my $s = $b->{timestamp} <=> $a->{timestamp};
1196         $s == 0 ?
1197              $b->{issuedate} <=> $a->{issuedate} : $s;
1198     } @issues;
1199
1200     my ($letter_code, %repeat);
1201     if ( $quickslip ) {
1202         $letter_code = 'ISSUEQSLIP';
1203         %repeat =  (
1204             'checkedout' => [ map {
1205                 'biblio'       => $_,
1206                 'items'        => $_,
1207                 'biblioitems'  => $_,
1208                 'issues'       => $_,
1209             }, grep { $_->{'now'} } @issues ],
1210         );
1211     }
1212     else {
1213         $letter_code = 'ISSUESLIP';
1214         %repeat =  (
1215             'checkedout' => [ map {
1216                 'biblio'       => $_,
1217                 'items'        => $_,
1218                 'biblioitems'  => $_,
1219                 'issues'       => $_,
1220             }, grep { !$_->{'overdue'} } @issues ],
1221
1222             'overdue' => [ map {
1223                 'biblio'       => $_,
1224                 'items'        => $_,
1225                 'biblioitems'  => $_,
1226                 'issues'       => $_,
1227             }, grep { $_->{'overdue'} } @issues ],
1228
1229             'news' => [ map {
1230                 $_->{'timestamp'} = $_->{'newdate'};
1231                 { opac_news => $_ }
1232             } @{ GetNewsToDisplay("slip",$branch) } ],
1233         );
1234     }
1235
1236     return  C4::Letters::GetPreparedLetter (
1237         module => 'circulation',
1238         letter_code => $letter_code,
1239         branchcode => $branch,
1240         lang => $patron->lang,
1241         tables => {
1242             'branches'    => $branch,
1243             'borrowers'   => $borrowernumber,
1244         },
1245         repeat => \%repeat,
1246     );
1247 }
1248
1249 =head2 AddMember_Auto
1250
1251 =cut
1252
1253 sub AddMember_Auto {
1254     my ( %borrower ) = @_;
1255
1256     $borrower{'cardnumber'} ||= fixup_cardnumber();
1257
1258     $borrower{'borrowernumber'} = AddMember(%borrower);
1259
1260     return ( %borrower );
1261 }
1262
1263 =head2 AddMember_Opac
1264
1265 =cut
1266
1267 sub AddMember_Opac {
1268     my ( %borrower ) = @_;
1269
1270     $borrower{'categorycode'} //= C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1271     if (not defined $borrower{'password'}){
1272         my $sr = new String::Random;
1273         $sr->{'A'} = [ 'A'..'Z', 'a'..'z' ];
1274         my $password = $sr->randpattern("AAAAAAAAAA");
1275         $borrower{'password'} = $password;
1276     }
1277
1278     %borrower = AddMember_Auto(%borrower);
1279
1280     return ( $borrower{'borrowernumber'}, $borrower{'password'} );
1281 }
1282
1283 =head2 DeleteExpiredOpacRegistrations
1284
1285     Delete accounts that haven't been upgraded from the 'temporary' category
1286     Returns the number of removed patrons
1287
1288 =cut
1289
1290 sub DeleteExpiredOpacRegistrations {
1291
1292     my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
1293     my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1294
1295     return 0 if not $category_code or not defined $delay or $delay eq q||;
1296
1297     my $query = qq|
1298 SELECT borrowernumber
1299 FROM borrowers
1300 WHERE categorycode = ? AND DATEDIFF( NOW(), dateenrolled ) > ? |;
1301
1302     my $dbh = C4::Context->dbh;
1303     my $sth = $dbh->prepare($query);
1304     $sth->execute( $category_code, $delay );
1305     my $cnt=0;
1306     while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
1307         Koha::Patrons->find($borrowernumber)->delete;
1308         $cnt++;
1309     }
1310     return $cnt;
1311 }
1312
1313 =head2 DeleteUnverifiedOpacRegistrations
1314
1315     Delete all unverified self registrations in borrower_modifications,
1316     older than the specified number of days.
1317
1318 =cut
1319
1320 sub DeleteUnverifiedOpacRegistrations {
1321     my ( $days ) = @_;
1322     my $dbh = C4::Context->dbh;
1323     my $sql=qq|
1324 DELETE FROM borrower_modifications
1325 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
1326     my $cnt=$dbh->do($sql, undef, ($days) );
1327     return $cnt eq '0E0'? 0: $cnt;
1328 }
1329
1330 sub GetOverduesForPatron {
1331     my ( $borrowernumber ) = @_;
1332
1333     my $sql = "
1334         SELECT *
1335         FROM issues, items, biblio, biblioitems
1336         WHERE items.itemnumber=issues.itemnumber
1337           AND biblio.biblionumber   = items.biblionumber
1338           AND biblio.biblionumber   = biblioitems.biblionumber
1339           AND issues.borrowernumber = ?
1340           AND date_due < NOW()
1341     ";
1342
1343     my $sth = C4::Context->dbh->prepare( $sql );
1344     $sth->execute( $borrowernumber );
1345
1346     return $sth->fetchall_arrayref({});
1347 }
1348
1349 END { }    # module clean-up code here (global destructor)
1350
1351 1;
1352
1353 __END__
1354
1355 =head1 AUTHOR
1356
1357 Koha Team
1358
1359 =cut