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