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