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