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