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