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