Bug 15344: Remove some other calls of GetMemberDetails from pl scripts
[koha.git] / C4 / Members.pm
index b89d721..fafa02f 100644 (file)
@@ -6,25 +6,23 @@ package C4::Members;
 #
 # This file is part of Koha.
 #
-# Koha is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 2 of the License, or (at your option) any later
-# version.
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
 #
-# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License along
-# with Koha; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
 
 
 use strict;
 #use warnings; FIXME - Bug 2505
 use C4::Context;
-use C4::Dates qw(format_date_in_iso format_date);
-use Digest::MD5 qw(md5_base64);
 use String::Random qw( random_string );
 use Date::Calc qw/Today Add_Delta_YM check_date Date_to_Days/;
 use C4::Log; # logaction
@@ -33,13 +31,20 @@ use C4::Reserves;
 use C4::Accounts;
 use C4::Biblio;
 use C4::Letters;
-use C4::SQLHelper qw(InsertInTable UpdateInTable SearchInTable);
-use C4::Members::Attributes qw(SearchIdMatchingAttribute);
+use C4::Members::Attributes qw(SearchIdMatchingAttribute UpdateBorrowerAttribute);
 use C4::NewsChannels; #get slip news
 use DateTime;
-use DateTime::Format::DateParse;
+use Koha::Database;
 use Koha::DateUtils;
+use Koha::Borrower::Debarments qw(IsDebarred);
 use Text::Unaccent qw( unac_string );
+use Koha::AuthUtils qw(hash_password);
+use Koha::Database;
+
+use Module::Load::Conditional qw( can_load );
+if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
+   warn "Unable to load Koha::NorwegianPatronDB";
+}
 
 our ($VERSION,@ISA,@EXPORT,@EXPORT_OK,$debug);
 
@@ -61,16 +66,14 @@ BEGIN {
         &GetPendingIssues
         &GetAllIssues
 
-        &get_institutions
         &getzipnamecity
         &getidcity
 
         &GetFirstValidEmailAddress
+        &GetNoticeEmailAddress
 
         &GetAge
         &GetCities
-        &GetRoadTypes
-        &GetRoadTypeDetails
         &GetSortDetails
         &GetTitles
 
@@ -89,11 +92,12 @@ BEGIN {
         GetBorrowerCategorycode
         &GetBorrowercategoryList
 
-        &GetBorrowersWhoHaveNotBorrowedSince
+        &GetBorrowersToExpunge
         &GetBorrowersWhoHaveNeverBorrowed
         &GetBorrowersWithIssuesHistoryOlderThan
 
         &GetExpiryDate
+        &GetUpcomingMembershipExpires
 
         &AddMessage
         &DeleteMessage
@@ -102,6 +106,9 @@ BEGIN {
 
         &IssueSlip
         GetBorrowersWithEmail
+
+        HasOverdues
+        GetOverduesForPatron
     );
 
     #Modify data
@@ -120,7 +127,6 @@ BEGIN {
     push @EXPORT, qw(
         &AddMember
         &AddMember_Opac
-        &add_member_orgs
         &MoveMemberToDeleted
         &ExtendMemberSubscriptionTo
     );
@@ -131,8 +137,6 @@ BEGIN {
         &checkuserpassword
         &Check_Userid
         &Generate_Userid
-        &fixEthnicity
-        &ethnicitycategories
         &fixup_cardnumber
         &checkcardnumber
     );
@@ -152,141 +156,6 @@ This module contains routines for adding, modifying and deleting members/patrons
 
 =head1 FUNCTIONS
 
-=head2 Search
-
-  $borrowers_result_array_ref = &Search($filter,$orderby, $limit, 
-                       $columns_out, $search_on_fields,$searchtype);
-
-Looks up patrons (borrowers) on filter. A wrapper for SearchInTable('borrowers').
-
-For C<$filter>, C<$orderby>, C<$limit>, C<&columns_out>, C<&search_on_fields> and C<&searchtype>
-refer to C4::SQLHelper:SearchInTable().
-
-Special C<$filter> key '' is effectively expanded to search on surname firstname othernamescw
-and cardnumber unless C<&search_on_fields> is defined
-
-Examples:
-
-  $borrowers = Search('abcd', 'cardnumber');
-
-  $borrowers = Search({''=>'abcd', category_type=>'I'}, 'surname');
-
-=cut
-
-sub _express_member_find {
-    my ($filter) = @_;
-
-    # this is used by circulation everytime a new borrowers cardnumber is scanned
-    # so we can check an exact match first, if that works return, otherwise do the rest
-    my $dbh   = C4::Context->dbh;
-    my $query = "SELECT borrowernumber FROM borrowers WHERE cardnumber = ?";
-    if ( my $borrowernumber = $dbh->selectrow_array($query, undef, $filter) ) {
-        return( {"borrowernumber"=>$borrowernumber} );
-    }
-
-    my ($search_on_fields, $searchtype);
-    if ( length($filter) == 1 ) {
-        $search_on_fields = [ qw(surname) ];
-        $searchtype = 'start_with';
-    } else {
-        $search_on_fields = [ qw(surname firstname othernames cardnumber) ];
-        $searchtype = 'contain';
-    }
-
-    return (undef, $search_on_fields, $searchtype);
-}
-
-sub Search {
-    my ( $filter, $orderby, $limit, $columns_out, $search_on_fields, $searchtype ) = @_;
-
-    my $search_string;
-    my $found_borrower;
-
-    if ( my $fr = ref $filter ) {
-        if ( $fr eq "HASH" ) {
-            if ( my $search_string = $filter->{''} ) {
-                my ($member_filter, $member_search_on_fields, $member_searchtype) = _express_member_find($search_string);
-                if ($member_filter) {
-                    $filter = $member_filter;
-                    $found_borrower = 1;
-                } else {
-                    $search_on_fields ||= $member_search_on_fields;
-                    $searchtype ||= $member_searchtype;
-                }
-            }
-        }
-        else {
-            $search_string = $filter;
-        }
-    }
-    else {
-        $search_string = $filter;
-        my ($member_filter, $member_search_on_fields, $member_searchtype) = _express_member_find($search_string);
-        if ($member_filter) {
-            $filter = $member_filter;
-            $found_borrower = 1;
-        } else {
-            $search_on_fields ||= $member_search_on_fields;
-            $searchtype ||= $member_searchtype;
-        }
-    }
-
-    if ( !$found_borrower && C4::Context->preference('ExtendedPatronAttributes') && $search_string ) {
-        my $matching_records = C4::Members::Attributes::SearchIdMatchingAttribute($search_string);
-        if(scalar(@$matching_records)>0) {
-            if ( my $fr = ref $filter ) {
-                if ( $fr eq "HASH" ) {
-                    my %f = %$filter;
-                    $filter = [ $filter ];
-                    delete $f{''};
-                    push @$filter, { %f, "borrowernumber"=>$$matching_records };
-                }
-                else {
-                    push @$filter, {"borrowernumber"=>$matching_records};
-                }
-            }
-            else {
-                $filter = [ $filter ];
-                push @$filter, {"borrowernumber"=>$matching_records};
-            }
-               }
-    }
-
-    # $showallbranches was not used at the time SearchMember() was mainstreamed into Search().
-    # Mentioning for the reference
-
-    if ( C4::Context->preference("IndependantBranches") ) { # && !$showallbranches){
-        if ( my $userenv = C4::Context->userenv ) {
-            my $branch =  $userenv->{'branch'};
-            if ( ($userenv->{flags} % 2 !=1) &&
-                 $branch && $branch ne "insecure" ){
-
-                if (my $fr = ref $filter) {
-                    if ( $fr eq "HASH" ) {
-                        $filter->{branchcode} = $branch;
-                    }
-                    else {
-                        foreach (@$filter) {
-                            $_ = { '' => $_ } unless ref $_;
-                            $_->{branchcode} = $branch;
-                        }
-                    }
-                }
-                else {
-                    $filter = { '' => $filter, branchcode => $branch };
-                }
-            }      
-        }
-    }
-
-    if ($found_borrower) {
-        $searchtype = "exact";
-    }
-    $searchtype ||= "start_with";
-
-       return SearchInTable( "borrowers", $filter, $orderby, $limit, $columns_out, $search_on_fields, $searchtype );
-}
-
 =head2 GetMemberDetails
 
 ($borrower) = &GetMemberDetails($borrowernumber, $cardnumber);
@@ -325,18 +194,39 @@ sub GetMemberDetails {
     my $query;
     my $sth;
     if ($borrowernumber) {
-        $sth = $dbh->prepare("SELECT borrowers.*,category_type,categories.description,reservefee,enrolmentperiod FROM borrowers LEFT JOIN categories ON borrowers.categorycode=categories.categorycode WHERE  borrowernumber=?");
+        $sth = $dbh->prepare("
+            SELECT borrowers.*,
+                   category_type,
+                   categories.description,
+                   categories.BlockExpiredPatronOpacActions,
+                   reservefee,
+                   enrolmentperiod
+            FROM borrowers
+            LEFT JOIN categories ON borrowers.categorycode=categories.categorycode
+            WHERE borrowernumber = ?
+        ");
         $sth->execute($borrowernumber);
     }
     elsif ($cardnumber) {
-        $sth = $dbh->prepare("SELECT borrowers.*,category_type,categories.description,reservefee,enrolmentperiod FROM borrowers LEFT JOIN categories ON borrowers.categorycode=categories.categorycode WHERE cardnumber=?");
+        $sth = $dbh->prepare("
+            SELECT borrowers.*,
+                   category_type,
+                   categories.description,
+                   categories.BlockExpiredPatronOpacActions,
+                   reservefee,
+                   enrolmentperiod
+            FROM borrowers
+            LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
+            WHERE cardnumber = ?
+        ");
         $sth->execute($cardnumber);
     }
     else {
         return;
     }
     my $borrower = $sth->fetchrow_hashref;
-    my ($amount) = GetMemberAccountRecords( $borrowernumber);
+    return unless $borrower;
+    my ($amount) = GetMemberAccountRecords($borrower->{borrowernumber});
     $borrower->{'amountoutstanding'} = $amount;
     # FIXME - patronflags calls GetMemberAccountRecords... just have patronflags return $amount
     my $flags = patronflags( $borrower);
@@ -352,15 +242,17 @@ sub GetMemberDetails {
     $borrower->{'flags'}     = $flags;
     $borrower->{'authflags'} = $accessflagshash;
 
-    # For the purposes of making templates easier, we'll define a
-    # 'showname' which is the alternate form the user's first name if 
-    # 'other name' is defined.
-    if ($borrower->{category_type} eq 'I') {
-        $borrower->{'showname'} = $borrower->{'othernames'};
-        $borrower->{'showname'} .= " $borrower->{'firstname'}" if $borrower->{'firstname'};
-    } else {
-        $borrower->{'showname'} = $borrower->{'firstname'};
-    }
+    # Handle setting the true behavior for BlockExpiredPatronOpacActions
+    $borrower->{'BlockExpiredPatronOpacActions'} =
+      C4::Context->preference('BlockExpiredPatronOpacActions')
+      if ( $borrower->{'BlockExpiredPatronOpacActions'} == -1 );
+
+    $borrower->{'is_expired'} = 0;
+    $borrower->{'is_expired'} = 1 if
+      defined($borrower->{dateexpiry}) &&
+      $borrower->{'dateexpiry'} ne '0000-00-00' &&
+      Date_to_Days( Today() ) >
+      Date_to_Days( split /-/, $borrower->{'dateexpiry'} );
 
     return ($borrower);    #, $flags, $accessflagshash);
 }
@@ -432,21 +324,21 @@ sub patronflags {
     my %flags;
     my ( $patroninformation) = @_;
     my $dbh=C4::Context->dbh;
-    my ($amount) = GetMemberAccountRecords( $patroninformation->{'borrowernumber'});
-    if ( $amount > 0 ) {
+    my ($balance, $owing) = GetMemberAccountBalance( $patroninformation->{'borrowernumber'});
+    if ( $owing > 0 ) {
         my %flaginfo;
         my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
-        $flaginfo{'message'} = sprintf "Patron owes \$%.02f", $amount;
-        $flaginfo{'amount'}  = sprintf "%.02f", $amount;
-        if ( $amount > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
+        $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
+        $flaginfo{'amount'}  = sprintf "%.02f", $owing;
+        if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
             $flaginfo{'noissues'} = 1;
         }
         $flags{'CHARGES'} = \%flaginfo;
     }
-    elsif ( $amount < 0 ) {
+    elsif ( $balance < 0 ) {
         my %flaginfo;
-        $flaginfo{'message'} = sprintf "Patron has credit of \$%.02f", -$amount;
-        $flaginfo{'amount'}  = sprintf "%.02f", $amount;
+        $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
+        $flaginfo{'amount'}  = sprintf "%.02f", $balance;
         $flags{'CREDITS'} = \%flaginfo;
     }
     if (   $patroninformation->{'gonenoaddress'}
@@ -573,7 +465,8 @@ sub GetMember {
 
  C<GetMemberRelatives> returns a borrowersnumber's list of guarantor/guarantees of the member given in parameter
 
-=cut 
+=cut
+
 sub GetMemberRelatives {
     my $borrowernumber = shift;
     my $dbh = C4::Context->dbh;
@@ -612,24 +505,19 @@ sub GetMemberRelatives {
 
   my ($block_status, $count) = IsMemberBlocked( $borrowernumber );
 
-Returns whether a patron has overdue items that may result
-in a block or whether the patron has active fine days
-that would block circulation privileges.
+Returns whether a patron is restricted or has overdue items that may result
+in a block of circulation privileges.
 
 C<$block_status> can have the following values:
 
-1 if the patron has outstanding fine days, in which case C<$count> is the number of them
+1 if the patron is currently restricted, in which case
+C<$count> is the expiration date (9999-12-31 for indefinite)
 
 -1 if the patron has overdue items, in which case C<$count> is the number of them
 
 0 if the patron has no overdue items or outstanding fine days, in which case C<$count> is 0
 
-Outstanding fine days are checked before current overdue items
-are.
-
-FIXME: this needs to be split into two functions; a potential block
-based on the number of current overdue items could be orthogonal
-to a block based on whether the patron has any fine days accrued.
+Existing active restrictions are checked before current overdue items.
 
 =cut
 
@@ -637,7 +525,7 @@ sub IsMemberBlocked {
     my $borrowernumber = shift;
     my $dbh            = C4::Context->dbh;
 
-    my $blockeddate = CheckBorrowerDebarred($borrowernumber);
+    my $blockeddate = Koha::Borrower::Debarments::IsDebarred($borrowernumber);
 
     return ( 1, $blockeddate ) if $blockeddate;
 
@@ -696,10 +584,42 @@ sub GetMemberIssuesAndFines {
     return ($overdue_count, $issue_count, $total_fines);
 }
 
-sub columns(;$) {
-    return @{C4::Context->dbh->selectcol_arrayref("SHOW columns from borrowers")};
+
+=head2 columns
+
+  my @columns = C4::Member::columns();
+
+Returns an array of borrowers' table columns on success,
+and an empty array on failure.
+
+=cut
+
+sub columns {
+
+    # Pure ANSI SQL goodness.
+    my $sql = 'SELECT * FROM borrowers WHERE 1=0;';
+
+    # Get the database handle.
+    my $dbh = C4::Context->dbh;
+
+    # Run the SQL statement to load STH's readonly properties.
+    my $sth = $dbh->prepare($sql);
+    my $rv = $sth->execute();
+
+    # This only fails if the table doesn't exist.
+    # This will always be called AFTER an install or upgrade,
+    # so borrowers will exist!
+    my @data;
+    if ($sth->{NUM_OF_FIELDS}>0) {
+        @data = @{$sth->{NAME}};
+    }
+    else {
+        @data = ();
+    }
+    return @data;
 }
 
+
 =head2 ModMember
 
   my $success = ModMember(borrowernumber => $borrowernumber,
@@ -719,11 +639,30 @@ sub ModMember {
         if ($data{password} eq '****' or $data{password} eq '') {
             delete $data{password};
         } else {
-            $data{password} = md5_base64($data{password});
+            if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
+                # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
+                Koha::NorwegianPatronDB::NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
+            }
+            $data{password} = hash_password($data{password});
         }
     }
-       my $execute_success=UpdateInTable("borrowers",\%data);
-    if ($execute_success) { # only proceed if the update was a success
+    my $old_categorycode = GetBorrowerCategorycode( $data{borrowernumber} );
+
+    # get only the columns of a borrower
+    my $schema = Koha::Database->new()->schema;
+    my @columns = $schema->source('Borrower')->columns;
+    my $new_borrower = { map { join(' ', @columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) };
+    delete $new_borrower->{flags};
+
+    $new_borrower->{dateofbirth}  ||= undef if exists $new_borrower->{dateofbirth};
+    $new_borrower->{dateenrolled} ||= undef if exists $new_borrower->{dateenrolled};
+    $new_borrower->{dateexpiry}   ||= undef if exists $new_borrower->{dateexpiry};
+    $new_borrower->{debarred}     ||= undef if exists $new_borrower->{debarred};
+    my $rs = $schema->resultset('Borrower')->search({
+        borrowernumber => $new_borrower->{borrowernumber},
+     });
+    my $execute_success = $rs->update($new_borrower);
+    if ($execute_success ne '0E0') { # only proceed if the update was a success
         # ok if its an adult (type) it may have borrowers that depend on it as a guarantor
         # so when we update information for an adult we should check for guarantees and update the relevant part
         # of their records, ie addresses and phone numbers
@@ -732,17 +671,48 @@ sub ModMember {
             # is adult check guarantees;
             UpdateGuarantees(%data);
         }
+
+        # If the patron changes to a category with enrollment fee, we add a fee
+        if ( $data{categorycode} and $data{categorycode} ne $old_categorycode ) {
+            if ( C4::Context->preference('FeeOnChangePatronCategory') ) {
+                AddEnrolmentFeeIfNeeded( $data{categorycode}, $data{borrowernumber} );
+            }
+        }
+
+        # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
+        # cronjob will use for syncing with NL
+        if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
+            my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
+                'synctype'       => 'norwegianpatrondb',
+                'borrowernumber' => $data{'borrowernumber'}
+            });
+            # Do not set to "edited" if syncstatus is "new". We need to sync as new before
+            # we can sync as changed. And the "new sync" will pick up all changes since
+            # the patron was created anyway.
+            if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
+                $borrowersync->update( { 'syncstatus' => 'edited' } );
+            }
+            # Set the value of 'sync'
+            $borrowersync->update( { 'sync' => $data{'sync'} } );
+            # Try to do the live sync
+            Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
+        }
+
         logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog");
     }
     return $execute_success;
 }
 
-
 =head2 AddMember
 
   $borrowernumber = &AddMember(%borrower);
 
 insert new borrower into table
+
+(%borrower keys are database columns. Database columns could be
+different in different versions. Please look into database for correct
+column names.)
+
 Returns the borrowernumber upon success
 
 Returns as undef upon any db error without further processing
@@ -753,66 +723,116 @@ Returns as undef upon any db error without further processing
 sub AddMember {
     my (%data) = @_;
     my $dbh = C4::Context->dbh;
+    my $schema = Koha::Database->new()->schema;
 
     # generate a proper login if none provided
-    $data{'userid'} = Generate_Userid($data{'borrowernumber'}, $data{'firstname'}, $data{'surname'}) if $data{'userid'} eq '';
+    $data{'userid'} = Generate_Userid( $data{'borrowernumber'}, $data{'firstname'}, $data{'surname'} )
+      if ( $data{'userid'} eq '' || !Check_Userid( $data{'userid'} ) );
 
     # add expiration date if it isn't already there
     unless ( $data{'dateexpiry'} ) {
-        $data{'dateexpiry'} = GetExpiryDate( $data{'categorycode'}, C4::Dates->new()->output("iso") );
+        $data{'dateexpiry'} = GetExpiryDate( $data{'categorycode'}, output_pref( { dt => dt_from_string, dateonly => 1, dateformat => 'iso' } ) );
     }
 
     # add enrollment date if it isn't already there
     unless ( $data{'dateenrolled'} ) {
-        $data{'dateenrolled'} = C4::Dates->new()->output("iso");
+        $data{'dateenrolled'} = output_pref( { dt => dt_from_string, dateonly => 1, dateformat => 'iso' } );
     }
 
+    my $patron_category = $schema->resultset('Category')->find( $data{'categorycode'} );
+    $data{'privacy'} =
+        $patron_category->default_privacy() eq 'default' ? 1
+      : $patron_category->default_privacy() eq 'never'   ? 2
+      : $patron_category->default_privacy() eq 'forever' ? 0
+      :                                                    undef;
+    # Make a copy of the plain text password for later use
+    my $plain_text_password = $data{'password'};
+
     # create a disabled account if no password provided
-    $data{'password'} = ($data{'password'})? md5_base64($data{'password'}) : '!';
-    $data{'borrowernumber'}=InsertInTable("borrowers",\%data);
+    $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!';
+
+    # we don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00
+    $data{'dateofbirth'} = undef if( not $data{'dateofbirth'} );
+    $data{'debarred'} = undef if ( not $data{'debarred'} );
 
+    # get only the columns of Borrower
+    my @columns = $schema->source('Borrower')->columns;
+    my $new_member = { map { join(' ',@columns) =~ /$_/ ? ( $_ => $data{$_} )  : () } keys(%data) } ;
+    delete $new_member->{borrowernumber};
+
+    my $rs = $schema->resultset('Borrower');
+    $data{borrowernumber} = $rs->create($new_member)->id;
+
+    # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
+    # cronjob will use for syncing with NL
+    if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
+        Koha::Database->new->schema->resultset('BorrowerSync')->create({
+            'borrowernumber' => $data{'borrowernumber'},
+            'synctype'       => 'norwegianpatrondb',
+            'sync'           => 1,
+            'syncstatus'     => 'new',
+            'hashed_pin'     => Koha::NorwegianPatronDB::NLEncryptPIN( $plain_text_password ),
+        });
+    }
 
     # mysql_insertid is probably bad.  not necessarily accurate and mysql-specific at best.
     logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog");
-    
-    # check for enrollment fee & add it if needed
-    my $sth = $dbh->prepare("SELECT enrolmentfee FROM categories WHERE categorycode=?");
-    $sth->execute($data{'categorycode'});
-    my ($enrolmentfee) = $sth->fetchrow;
-    if ($sth->err) {
-        warn sprintf('Database returned the following error: %s', $sth->errstr);
-        return;
-    }
-    if ($enrolmentfee && $enrolmentfee > 0) {
-        # insert fee in patron debts
-        manualinvoice($data{'borrowernumber'}, '', '', 'A', $enrolmentfee);
-    }
 
-    return $data{'borrowernumber'};
+    AddEnrolmentFeeIfNeeded( $data{categorycode}, $data{borrowernumber} );
+
+    return $data{borrowernumber};
 }
 
+=head2 Check_Userid
+
+    my $uniqueness = Check_Userid($userid,$borrowernumber);
+
+    $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 != '').
+
+    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.
+
+    return :
+        0 for not unique (i.e. this $userid already exists)
+        1 for unique (i.e. this $userid does not exist, or this $userid/$borrowernumber combination already exists)
+
+=cut
 
 sub Check_Userid {
-    my ($uid,$member) = @_;
-    my $dbh = C4::Context->dbh;
-    # Make sure the userid chosen is unique and not theirs if non-empty. If it is not,
-    # Then we need to tell the user and have them create a new one.
-    my $sth =
-      $dbh->prepare(
-        "SELECT * FROM borrowers WHERE userid=? AND borrowernumber != ?");
-    $sth->execute( $uid, $member );
-    if ( ( $uid ne '' ) && ( my $row = $sth->fetchrow_hashref ) ) {
-        return 0;
-    }
-    else {
-        return 1;
-    }
+    my ( $uid, $borrowernumber ) = @_;
+
+    return 0 unless ($uid); # userid is a unique column, we should assume NULL is not unique
+
+    return 0 if ( $uid eq C4::Context->config('user') );
+
+    my $rs = Koha::Database->new()->schema()->resultset('Borrower');
+
+    my $params;
+    $params->{userid} = $uid;
+    $params->{borrowernumber} = { '!=' => $borrowernumber } if ($borrowernumber);
+
+    my $count = $rs->count( $params );
+
+    return $count ? 0 : 1;
 }
 
+=head2 Generate_Userid
+
+    my $newuid = Generate_Userid($borrowernumber, $firstname, $surname);
+
+    Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
+
+    $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.
+
+    return :
+        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).
+
+=cut
+
 sub Generate_Userid {
   my ($borrowernumber, $firstname, $surname) = @_;
   my $newuid;
   my $offset = 0;
+  #The script will "do" the following code and increment the $offset until Check_Userid = 1 (i.e. until $newuid comes back as unique)
   do {
     $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
     $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
@@ -1050,14 +1070,17 @@ sub GetPendingIssues {
     my $sth = C4::Context->dbh->prepare($query);
     $sth->execute(@borrowernumbers);
     my $data = $sth->fetchall_arrayref({});
-    my $tz = C4::Context->tz();
-    my $today = DateTime->now( time_zone => $tz);
+    my $today = dt_from_string;
     foreach (@{$data}) {
         if ($_->{issuedate}) {
             $_->{issuedate} = dt_from_string($_->{issuedate}, 'sql');
         }
+        $_->{date_due_sql} = $_->{date_due};
+        # FIXME no need to have this value
         $_->{date_due} or next;
-        $_->{date_due} = DateTime::Format::DateParse->parse_datetime($_->{date_due}, $tz->name());
+        $_->{date_due_sql} = $_->{date_due};
+        # FIXME no need to have this value
+        $_->{date_due} = dt_from_string($_->{date_due}, 'sql');
         if ( DateTime->compare($_->{date_due}, $today) == -1 ) {
             $_->{overdue} = 1;
         }
@@ -1088,6 +1111,9 @@ C<items> tables of the Koha database.
 sub GetAllIssues {
     my ( $borrowernumber, $order, $limit ) = @_;
 
+    return unless $borrowernumber;
+    $order = 'date_due desc' unless $order;
+
     my $dbh = C4::Context->dbh;
     my $query =
 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
@@ -1128,9 +1154,8 @@ total amount outstanding for all of the account lines.
 
 =cut
 
-#'
 sub GetMemberAccountRecords {
-    my ($borrowernumber,$date) = @_;
+    my ($borrowernumber) = @_;
     my $dbh = C4::Context->dbh;
     my @acctlines;
     my $numlines = 0;
@@ -1138,14 +1163,10 @@ sub GetMemberAccountRecords {
                         SELECT * 
                         FROM accountlines 
                         WHERE borrowernumber=?);
-    my @bind = ($borrowernumber);
-    if ($date && $date ne ''){
-            $strsth.=" AND date < ? ";
-            push(@bind,$date);
-    }
-    $strsth.=" ORDER BY date desc,timestamp DESC";
+    $strsth.=" ORDER BY accountlines_id desc";
     my $sth= $dbh->prepare( $strsth );
-    $sth->execute( @bind );
+    $sth->execute( $borrowernumber );
+
     my $total = 0;
     while ( my $data = $sth->fetchrow_hashref ) {
         if ( $data->{itemnumber} ) {
@@ -1161,6 +1182,43 @@ sub GetMemberAccountRecords {
     return ( $total, \@acctlines,$numlines);
 }
 
+=head2 GetMemberAccountBalance
+
+  ($total_balance, $non_issue_balance, $other_charges) = &GetMemberAccountBalance($borrowernumber);
+
+Calculates amount immediately owing by the patron - non-issue charges.
+Based on GetMemberAccountRecords.
+Charges exempt from non-issue are:
+* Res (reserves)
+* Rent (rental) if RentalsInNoissuesCharge syspref is set to false
+* Manual invoices if ManInvInNoissuesCharge syspref is set to false
+
+=cut
+
+sub GetMemberAccountBalance {
+    my ($borrowernumber) = @_;
+
+    my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
+
+    my @not_fines;
+    push @not_fines, 'Res' unless C4::Context->preference('HoldsInNoissuesCharge');
+    push @not_fines, 'Rent' unless C4::Context->preference('RentalsInNoissuesCharge');
+    unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
+        my $dbh = C4::Context->dbh;
+        my $man_inv_types = $dbh->selectcol_arrayref(qq{SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'});
+        push @not_fines, map substr($_, 0, $ACCOUNT_TYPE_LENGTH), @$man_inv_types;
+    }
+    my %not_fine = map {$_ => 1} @not_fines;
+
+    my ($total, $acctlines) = GetMemberAccountRecords($borrowernumber);
+    my $other_charges = 0;
+    foreach (@$acctlines) {
+        $other_charges += $_->{amountoutstanding} if $not_fine{ substr($_->{accounttype}, 0, $ACCOUNT_TYPE_LENGTH) };
+    }
+
+    return ( $total, $total - $other_charges, $other_charges);
+}
+
 =head2 GetBorNotifyAcctRecord
 
   ($total, $acctlines, $count) = &GetBorNotifyAcctRecord($params,$notifyid);
@@ -1245,26 +1303,60 @@ sub checkuniquemember {
 }
 
 sub checkcardnumber {
-    my ($cardnumber,$borrowernumber) = @_;
+    my ( $cardnumber, $borrowernumber ) = @_;
+
     # If cardnumber is null, we assume they're allowed.
-    return 0 if !defined($cardnumber);
+    return 0 unless defined $cardnumber;
+
     my $dbh = C4::Context->dbh;
     my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
     $query .= " AND borrowernumber <> ?" if ($borrowernumber);
-  my $sth = $dbh->prepare($query);
-  if ($borrowernumber) {
-   $sth->execute($cardnumber,$borrowernumber);
-  } else { 
-     $sth->execute($cardnumber);
-  } 
-    if (my $data= $sth->fetchrow_hashref()){
-        return 1;
-    }
-    else {
-        return 0;
-    }
-}  
+    my $sth = $dbh->prepare($query);
+    $sth->execute(
+        $cardnumber,
+        ( $borrowernumber ? $borrowernumber : () )
+    );
+
+    return 1 if $sth->fetchrow_hashref;
+
+    my ( $min_length, $max_length ) = get_cardnumber_length();
+    return 2
+        if length $cardnumber > $max_length
+        or length $cardnumber < $min_length;
+
+    return 0;
+}
+
+=head2 get_cardnumber_length
+
+    my ($min, $max) = C4::Members::get_cardnumber_length()
+
+Returns the minimum and maximum length for patron cardnumbers as
+determined by the CardnumberLength system preference, the
+BorrowerMandatoryField system preference, and the width of the
+database column.
+
+=cut
 
+sub get_cardnumber_length {
+    my ( $min, $max ) = ( 0, 16 ); # borrowers.cardnumber is a nullable varchar(16)
+    $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
+    if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
+        # Is integer and length match
+        if ( $cardnumber_length =~ m|^\d+$| ) {
+            $min = $max = $cardnumber_length
+                if $cardnumber_length >= $min
+                    and $cardnumber_length <= $max;
+        }
+        # Else assuming it is a range
+        elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
+            $min = $1 if $1 and $min < $1;
+            $max = $2 if $2 and $max > $2;
+        }
+
+    }
+    return ( $min, $max );
+}
 
 =head2 getzipnamecity (OUEST-PROVENCE)
 
@@ -1328,6 +1420,35 @@ sub GetFirstValidEmailAddress {
     }
 }
 
+=head2 GetNoticeEmailAddress
+
+  $email = GetNoticeEmailAddress($borrowernumber);
+
+Return the email address of borrower used for notices, given the borrowernumber.
+Returns the empty string if no email address.
+
+=cut
+
+sub GetNoticeEmailAddress {
+    my $borrowernumber = shift;
+
+    my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
+    # if syspref is set to 'first valid' (value == OFF), look up email address
+    if ( $which_address eq 'OFF' ) {
+        return GetFirstValidEmailAddress($borrowernumber);
+    }
+    # specified email address field
+    my $dbh = C4::Context->dbh;
+    my $sth = $dbh->prepare( qq{
+        SELECT $which_address AS primaryemail
+        FROM borrowers
+        WHERE borrowernumber=?
+    } );
+    $sth->execute($borrowernumber);
+    my $data = $sth->fetchrow_hashref;
+    return $data->{'primaryemail'} || '';
+}
+
 =head2 GetExpiryDate 
 
   $expirydate = GetExpiryDate($categorycode, $dateenrolled);
@@ -1355,26 +1476,28 @@ sub GetExpiryDate {
     }
 }
 
-=head2 checkuserpassword (OUEST-PROVENCE)
+=head2 GetUpcomingMembershipExpires
 
-check for the password and login are not used
-return the number of record 
-0=> NOT USED 1=> USED
+  my $upcoming_mem_expires = GetUpcomingMembershipExpires();
 
 =cut
 
-sub checkuserpassword {
-    my ( $borrowernumber, $userid, $password ) = @_;
-    $password = md5_base64($password);
+sub GetUpcomingMembershipExpires {
     my $dbh = C4::Context->dbh;
-    my $sth =
-      $dbh->prepare(
-"Select count(*) from borrowers where borrowernumber !=? and userid =? and password=? "
-      );
-    $sth->execute( $borrowernumber, $userid, $password );
-    my $number_rows = $sth->fetchrow;
-    return $number_rows;
+    my $days = C4::Context->preference("MembershipExpiryDaysNotice") || 0;
+    my $dateexpiry = output_pref({ dt => (dt_from_string()->add( days => $days)), dateformat => 'iso', dateonly => 1 });
 
+    my $query = "
+        SELECT borrowers.*, categories.description,
+        branches.branchname, branches.branchemail FROM borrowers
+        LEFT JOIN branches on borrowers.branchcode = branches.branchcode
+        LEFT JOIN categories on borrowers.categorycode = categories.categorycode
+        WHERE dateexpiry = ?;
+    ";
+    my $sth = $dbh->prepare($query);
+    $sth->execute($dateexpiry);
+    my $results = $sth->fetchall_arrayref({});
+    return $results;
 }
 
 =head2 GetborCatFromCatType
@@ -1464,6 +1587,7 @@ sub GetBorrowercategory {
     $categorycode = &GetBorrowerCategoryCode( $borrowernumber );
 
 Given the borrowernumber, the function returns the corresponding categorycode
+
 =cut
 
 sub GetBorrowerCategorycode {
@@ -1504,54 +1628,6 @@ sub GetBorrowercategoryList {
     return $data;
 }    # sub getborrowercategory
 
-=head2 ethnicitycategories
-
-  ($codes_arrayref, $labels_hashref) = &ethnicitycategories();
-
-Looks up the different ethnic types in the database. Returns two
-elements: a reference-to-array, which lists the ethnicity codes, and a
-reference-to-hash, which maps the ethnicity codes to ethnicity
-descriptions.
-
-=cut
-
-#'
-
-sub ethnicitycategories {
-    my $dbh = C4::Context->dbh;
-    my $sth = $dbh->prepare("Select code,name from ethnicity order by name");
-    $sth->execute;
-    my %labels;
-    my @codes;
-    while ( my $data = $sth->fetchrow_hashref ) {
-        push @codes, $data->{'code'};
-        $labels{ $data->{'code'} } = $data->{'name'};
-    }
-    return ( \@codes, \%labels );
-}
-
-=head2 fixEthnicity
-
-  $ethn_name = &fixEthnicity($ethn_code);
-
-Takes an ethnicity code (e.g., "european" or "pi") and returns the
-corresponding descriptive name from the C<ethnicity> table in the
-Koha database ("European" or "Pacific Islander").
-
-=cut
-
-#'
-
-sub fixEthnicity {
-    my $ethnicity = shift;
-    return unless $ethnicity;
-    my $dbh       = C4::Context->dbh;
-    my $sth       = $dbh->prepare("Select name from ethnicity where code = ?");
-    $sth->execute($ethnicity);
-    my $data = $sth->fetchrow_hashref;
-    return $data->{'name'};
-}    # sub fixEthnicity
-
 =head2 GetAge
 
   $dateofbirth,$date = &GetAge($date);
@@ -1579,50 +1655,48 @@ sub GetAge{
     return $age;
 }    # sub get_age
 
-=head2 get_institutions
-
-  $insitutions = get_institutions();
-
-Just returns a list of all the borrowers of type I, borrownumber and name
-
-=cut
-
-#'
-sub get_institutions {
-    my $dbh = C4::Context->dbh();
-    my $sth =
-      $dbh->prepare(
-"SELECT borrowernumber,surname FROM borrowers WHERE categorycode=? ORDER BY surname"
-      );
-    $sth->execute('I');
-    my %orgs;
-    while ( my $data = $sth->fetchrow_hashref() ) {
-        $orgs{ $data->{'borrowernumber'} } = $data;
-    }
-    return ( \%orgs );
+=head2 SetAge
 
-}    # sub get_institutions
+  $borrower = C4::Members::SetAge($borrower, $datetimeduration);
+  $borrower = C4::Members::SetAge($borrower, '0015-12-10');
+  $borrower = C4::Members::SetAge($borrower, $datetimeduration, $datetime_reference);
 
-=head2 add_member_orgs
+  eval { $borrower = C4::Members::SetAge($borrower, '015-1-10'); };
+  if ($@) {print $@;} #Catch a bad ISO Date or kill your script!
 
-  add_member_orgs($borrowernumber,$borrowernumbers);
+This function sets the borrower's dateofbirth to match the given age.
+Optionally relative to the given $datetime_reference.
 
-Takes a borrowernumber and a list of other borrowernumbers and inserts them into the borrowers_to_borrowers table
+@PARAM1 koha.borrowers-object
+@PARAM2 DateTime::Duration-object as the desired age
+        OR a ISO 8601 Date. (To make the API more pleasant)
+@PARAM3 DateTime-object as the relative date, defaults to now().
+RETURNS The given borrower reference @PARAM1.
+DIES    If there was an error with the ISO Date handling.
 
 =cut
 
 #'
-sub add_member_orgs {
-    my ( $borrowernumber, $otherborrowers ) = @_;
-    my $dbh   = C4::Context->dbh();
-    my $query =
-      "INSERT INTO borrowers_to_borrowers (borrower1,borrower2) VALUES (?,?)";
-    my $sth = $dbh->prepare($query);
-    foreach my $otherborrowernumber (@$otherborrowers) {
-        $sth->execute( $borrowernumber, $otherborrowernumber );
+sub SetAge{
+    my ( $borrower, $datetimeduration, $datetime_ref ) = @_;
+    $datetime_ref = DateTime->now() unless $datetime_ref;
+
+    if ($datetimeduration && ref $datetimeduration ne 'DateTime::Duration') {
+        if ($datetimeduration =~ /^(\d{4})-(\d{2})-(\d{2})/) {
+            $datetimeduration = DateTime::Duration->new(years => $1, months => $2, days => $3);
+        }
+        else {
+            die "C4::Members::SetAge($borrower, $datetimeduration), datetimeduration not a valid ISO 8601 Date!\n";
+        }
     }
 
-}    # sub add_member_orgs
+    my $new_datetime_ref = $datetime_ref->clone();
+    $new_datetime_ref->subtract_duration( $datetimeduration );
+
+    $borrower->{dateofbirth} = $new_datetime_ref->ymd();
+
+    return $borrower;
+}    # sub SetAge
 
 =head2 GetCities
 
@@ -1683,27 +1757,22 @@ sub GetSortDetails {
   $result = &MoveMemberToDeleted($borrowernumber);
 
 Copy the record from borrowers to deletedborrowers table.
+The routine returns 1 for success, undef for failure.
 
 =cut
 
-# FIXME: should do it in one SQL statement w/ subquery
-# Otherwise, we should return the @data on success
-
 sub MoveMemberToDeleted {
     my ($member) = shift or return;
-    my $dbh = C4::Context->dbh;
-    my $query = qq|SELECT * 
-          FROM borrowers 
-          WHERE borrowernumber=?|;
-    my $sth = $dbh->prepare($query);
-    $sth->execute($member);
-    my @data = $sth->fetchrow_array;
-    (@data) or return;  # if we got a bad borrowernumber, there's nothing to insert
-    $sth =
-      $dbh->prepare( "INSERT INTO deletedborrowers VALUES ("
-          . ( "?," x ( scalar(@data) - 1 ) )
-          . "?)" );
-    $sth->execute(@data);
+
+    my $schema       = Koha::Database->new()->schema();
+    my $borrowers_rs = $schema->resultset('Borrower');
+    $borrowers_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
+    my $borrower = $borrowers_rs->find($member);
+    return unless $borrower;
+
+    my $deleted = $schema->resultset('Deletedborrower')->create($borrower);
+
+    return $deleted ? 1 : undef;
 }
 
 =head2 DelMember
@@ -1737,6 +1806,40 @@ sub DelMember {
     return $sth->rows;
 }
 
+=head2 HandleDelBorrower
+
+     HandleDelBorrower($borrower);
+
+When a member is deleted (DelMember in Members.pm), you should call me first.
+This routine deletes/moves lists and entries for the deleted member/borrower.
+Lists owned by the borrower are deleted, but entries from the borrower to
+other lists are kept.
+
+=cut
+
+sub HandleDelBorrower {
+    my ($borrower)= @_;
+    my $query;
+    my $dbh = C4::Context->dbh;
+
+    #Delete all lists and all shares of this borrower
+    #Consistent with the approach Koha uses on deleting individual lists
+    #Note that entries in virtualshelfcontents added by this borrower to
+    #lists of others will be handled by a table constraint: the borrower
+    #is set to NULL in those entries.
+    $query="DELETE FROM virtualshelves WHERE owner=?";
+    $dbh->do($query,undef,($borrower));
+
+    #NOTE:
+    #We could handle the above deletes via a constraint too.
+    #But a new BZ report 11889 has been opened to discuss another approach.
+    #Instead of deleting we could also disown lists (based on a pref).
+    #In that way we could save shared and public lists.
+    #The current table constraints support that idea now.
+    #This pref should then govern the results of other routines/methods such as
+    #Koha::Virtualshelf->new->delete too.
+}
+
 =head2 ExtendMemberSubscriptionTo (OUEST-PROVENCE)
 
     $date = ExtendMemberSubscriptionTo($borrowerid, $date);
@@ -1752,8 +1855,9 @@ sub ExtendMemberSubscriptionTo {
     my $borrower = GetMember('borrowernumber'=>$borrowerid);
     unless ($date){
       $date = (C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry') ?
-                                        C4::Dates->new($borrower->{'dateexpiry'}, 'iso')->output("iso") :
-                                        C4::Dates->new()->output("iso");
+                                        eval { output_pref( { dt => dt_from_string( $borrower->{'dateexpiry'}  ), dateonly => 1, dateformat => 'iso' } ); }
+                                        :
+                                        output_pref( { dt => dt_from_string, dateonly => 1, dateformat => 'iso' } );
       $date = GetExpiryDate( $borrower->{'categorycode'}, $date );
     }
     my $sth = $dbh->do(<<EOF);
@@ -1761,61 +1865,14 @@ UPDATE borrowers
 SET  dateexpiry='$date' 
 WHERE borrowernumber='$borrowerid'
 EOF
-    # add enrolmentfee if needed
-    $sth = $dbh->prepare("SELECT enrolmentfee FROM categories WHERE categorycode=?");
-    $sth->execute($borrower->{'categorycode'});
-    my ($enrolmentfee) = $sth->fetchrow;
-    if ($enrolmentfee && $enrolmentfee > 0) {
-        # insert fee in patron debts
-        manualinvoice($borrower->{'borrowernumber'}, '', '', 'A', $enrolmentfee);
-    }
-     logaction("MEMBERS", "RENEW", $borrower->{'borrowernumber'}, "Membership renewed")if C4::Context->preference("BorrowersLog");
-    return $date if ($sth);
-    return 0;
-}
-
-=head2 GetRoadTypes (OUEST-PROVENCE)
-
-  ($idroadtypearrayref, $roadttype_hashref) = &GetRoadTypes();
-
-Looks up the different road type . Returns two
-elements: a reference-to-array, which lists the id_roadtype
-codes, and a reference-to-hash, which maps the road type of the road .
-
-=cut
-
-sub GetRoadTypes {
-    my $dbh   = C4::Context->dbh;
-    my $query = qq|
-SELECT roadtypeid,road_type 
-FROM roadtype 
-ORDER BY road_type|;
-    my $sth = $dbh->prepare($query);
-    $sth->execute();
-    my %roadtype;
-    my @id;
 
-    #    insert empty value to create a empty choice in cgi popup
+    AddEnrolmentFeeIfNeeded( $borrower->{categorycode}, $borrower->{borrowernumber} );
 
-    while ( my $data = $sth->fetchrow_hashref ) {
-
-        push @id, $data->{'roadtypeid'};
-        $roadtype{ $data->{'roadtypeid'} } = $data->{'road_type'};
-    }
-
-#test to know if the table contain some records if no the function return nothing
-    my $id = @id;
-    if ( $id eq 0 ) {
-        return ();
-    }
-    else {
-        unshift( @id, "" );
-        return ( \@id, \%roadtype );
-    }
+    logaction("MEMBERS", "RENEW", $borrower->{'borrowernumber'}, "Membership renewed")if C4::Context->preference("BorrowersLog");
+    return $date if ($sth);
+    return 0;
 }
 
-
-
 =head2 GetTitles (OUEST-PROVENCE)
 
   ($borrowertitle)= &GetTitles();
@@ -1838,19 +1895,19 @@ sub GetTitles {
 
 =head2 GetPatronImage
 
-    my ($imagedata, $dberror) = GetPatronImage($cardnumber);
+    my ($imagedata, $dberror) = GetPatronImage($borrowernumber);
 
-Returns the mimetype and binary image data of the image for the patron with the supplied cardnumber.
+Returns the mimetype and binary image data of the image for the patron with the supplied borrowernumber.
 
 =cut
 
 sub GetPatronImage {
-    my ($cardnumber) = @_;
-    warn "Cardnumber passed to GetPatronImage is $cardnumber" if $debug;
+    my ($borrowernumber) = @_;
+    warn "Borrowernumber passed to GetPatronImage is $borrowernumber" if $debug;
     my $dbh = C4::Context->dbh;
-    my $query = 'SELECT mimetype, imagefile FROM patronimage WHERE cardnumber = ?';
+    my $query = 'SELECT mimetype, imagefile FROM patronimage WHERE borrowernumber = ?';
     my $sth = $dbh->prepare($query);
-    $sth->execute($cardnumber);
+    $sth->execute($borrowernumber);
     my $imagedata = $sth->fetchrow_hashref;
     warn "Database error!" if $sth->errstr;
     return $imagedata, $sth->errstr;
@@ -1869,7 +1926,7 @@ sub PutPatronImage {
     my ($cardnumber, $mimetype, $imgfile) = @_;
     warn "Parameters passed in: Cardnumber=$cardnumber, Mimetype=$mimetype, " . ($imgfile ? "Imagefile" : "No Imagefile") if $debug;
     my $dbh = C4::Context->dbh;
-    my $query = "INSERT INTO patronimage (cardnumber, mimetype, imagefile) VALUES (?,?,?) ON DUPLICATE KEY UPDATE imagefile = ?;";
+    my $query = "INSERT INTO patronimage (borrowernumber, mimetype, imagefile) VALUES ( ( SELECT borrowernumber from borrowers WHERE cardnumber = ? ),?,?) ON DUPLICATE KEY UPDATE imagefile = ?;";
     my $sth = $dbh->prepare($query);
     $sth->execute($cardnumber,$mimetype,$imgfile,$imgfile);
     warn "Error returned inserting $cardnumber.$mimetype." if $sth->errstr;
@@ -1878,19 +1935,19 @@ sub PutPatronImage {
 
 =head2 RmPatronImage
 
-    my ($dberror) = RmPatronImage($cardnumber);
+    my ($dberror) = RmPatronImage($borrowernumber);
 
-Removes the image for the patron with the supplied cardnumber.
+Removes the image for the patron with the supplied borrowernumber.
 
 =cut
 
 sub RmPatronImage {
-    my ($cardnumber) = @_;
-    warn "Cardnumber passed to GetPatronImage is $cardnumber" if $debug;
+    my ($borrowernumber) = @_;
+    warn "Borrowernumber passed to GetPatronImage is $borrowernumber" if $debug;
     my $dbh = C4::Context->dbh;
-    my $query = "DELETE FROM patronimage WHERE cardnumber = ?;";
+    my $query = "DELETE FROM patronimage WHERE borrowernumber = ?;";
     my $sth = $dbh->prepare($query);
-    $sth->execute($cardnumber);
+    $sth->execute($borrowernumber);
     my $dberror = $sth->errstr;
     warn "Database error!" if $sth->errstr;
     return $dberror;
@@ -1915,32 +1972,9 @@ sub GetHideLostItemsPreference {
     return $hidelostitems;    
 }
 
-=head2 GetRoadTypeDetails (OUEST-PROVENCE)
-
-  ($roadtype) = &GetRoadTypeDetails($roadtypeid);
-
-Returns the description of roadtype
-C<&$roadtype>return description of road type
-C<&$roadtypeid>this is the value of roadtype s
-
-=cut
-
-sub GetRoadTypeDetails {
-    my ($roadtypeid) = @_;
-    my $dbh          = C4::Context->dbh;
-    my $query        = qq|
-SELECT road_type 
-FROM roadtype 
-WHERE roadtypeid=?|;
-    my $sth = $dbh->prepare($query);
-    $sth->execute($roadtypeid);
-    my $roadtype = $sth->fetchrow;
-    return ($roadtype);
-}
-
-=head2 GetBorrowersWhoHaveNotBorrowedSince
+=head2 GetBorrowersToExpunge
 
-  $borrowers = &GetBorrowersWhoHaveNotBorrowedSince(
+  $borrowers = &GetBorrowersToExpunge(
       not_borrowered_since => $not_borrowered_since,
       expired_before       => $expired_before,
       category_code        => $category_code,
@@ -1951,32 +1985,39 @@ WHERE roadtypeid=?|;
 
 =cut
 
-sub GetBorrowersWhoHaveNotBorrowedSince {
+sub GetBorrowersToExpunge {
     my $params = shift;
 
     my $filterdate     = $params->{'not_borrowered_since'};
     my $filterexpiry   = $params->{'expired_before'};
     my $filtercategory = $params->{'category_code'};
     my $filterbranch   = $params->{'branchcode'} ||
-                        ((C4::Context->preference('IndependantBranches') 
+                        ((C4::Context->preference('IndependentBranches')
                              && C4::Context->userenv 
-                             && C4::Context->userenv->{flags} % 2 !=1 
+                             && !C4::Context->IsSuperLibrarian()
                              && C4::Context->userenv->{branch})
                          ? C4::Context->userenv->{branch}
                          : "");  
 
     my $dbh   = C4::Context->dbh;
-    my $query = "
+    my $query = q|
         SELECT borrowers.borrowernumber,
                MAX(old_issues.timestamp) AS latestissue,
                MAX(issues.timestamp) AS currentissue
         FROM   borrowers
         JOIN   categories USING (categorycode)
+        LEFT JOIN (
+            SELECT guarantorid
+            FROM borrowers
+            WHERE guarantorid IS NOT NULL
+                AND guarantorid <> 0
+        ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
         LEFT JOIN old_issues USING (borrowernumber)
         LEFT JOIN issues USING (borrowernumber) 
         WHERE  category_type <> 'S'
-        AND borrowernumber NOT IN (SELECT guarantorid FROM borrowers WHERE guarantorid IS NOT NULL AND guarantorid <> 0)
-   ";
+        AND tmp.guarantorid IS NULL
+   |;
+
     my @query_params;
     if ( $filterbranch && $filterbranch ne "" ) {
         $query.= " AND borrowers.branchcode = ? ";
@@ -2024,9 +2065,9 @@ I<$result> is a ref to an array which all elements are a hasref.
 
 sub GetBorrowersWhoHaveNeverBorrowed {
     my $filterbranch = shift || 
-                        ((C4::Context->preference('IndependantBranches') 
+                        ((C4::Context->preference('IndependentBranches')
                              && C4::Context->userenv 
-                             && C4::Context->userenv->{flags} % 2 !=1 
+                             && !C4::Context->IsSuperLibrarian()
                              && C4::Context->userenv->{branch})
                          ? C4::Context->userenv->{branch}
                          : "");  
@@ -2074,9 +2115,9 @@ sub GetBorrowersWithIssuesHistoryOlderThan {
     my $dbh  = C4::Context->dbh;
     my $date = shift ||POSIX::strftime("%Y-%m-%d",localtime());
     my $filterbranch = shift || 
-                        ((C4::Context->preference('IndependantBranches') 
+                        ((C4::Context->preference('IndependentBranches')
                              && C4::Context->userenv 
-                             && C4::Context->userenv->{flags} % 2 !=1 
+                             && !C4::Context->IsSuperLibrarian()
                              && C4::Context->userenv->{branch})
                          ? C4::Context->userenv->{branch}
                          : "");  
@@ -2130,45 +2171,15 @@ sub GetBorrowersNamesAndLatestIssue {
     return $results;
 }
 
-=head2 DebarMember
-
-my $success = DebarMember( $borrowernumber, $todate );
-
-marks a Member as debarred, and therefore unable to checkout any more
-items.
-
-return :
-true on success, false on failure
-
-=cut
-
-sub DebarMember {
-    my $borrowernumber = shift;
-    my $todate         = shift;
-
-    return unless defined $borrowernumber;
-    return unless $borrowernumber =~ /^\d+$/;
-
-    return ModMember(
-        borrowernumber => $borrowernumber,
-        debarred       => $todate
-    );
-
-}
-
 =head2 ModPrivacy
 
-=over 4
-
-my $success = ModPrivacy( $borrowernumber, $privacy );
+  my $success = ModPrivacy( $borrowernumber, $privacy );
 
 Update the privacy of a patron.
 
 return :
 true on success, false on failure
 
-=back
-
 =cut
 
 sub ModPrivacy {
@@ -2244,8 +2255,7 @@ sub GetMessages {
     my @results;
 
     while ( my $data = $sth->fetchrow_hashref ) {
-        my $d = C4::Dates->new( $data->{message_date}, 'iso' );
-        $data->{message_date_formatted} = $d->output;
+        $data->{message_date_formatted} = output_pref( { dt => dt_from_string( $data->{message_date} ), dateonly => 1, dateformat => 'iso' } );
         push @results, $data;
     }
     return \@results;
@@ -2314,36 +2324,81 @@ sub DeleteMessage {
 
   $quickslip is boolean, to indicate whether we want a quick slip
 
+  IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
+
+  Both slips:
+
+      <<branches.*>>
+      <<borrowers.*>>
+
+  ISSUESLIP:
+
+      <checkedout>
+         <<biblio.*>>
+         <<items.*>>
+         <<biblioitems.*>>
+         <<issues.*>>
+      </checkedout>
+
+      <overdue>
+         <<biblio.*>>
+         <<items.*>>
+         <<biblioitems.*>>
+         <<issues.*>>
+      </overdue>
+
+      <news>
+         <<opac_news.*>>
+      </news>
+
+  ISSUEQSLIP:
+
+      <checkedout>
+         <<biblio.*>>
+         <<items.*>>
+         <<biblioitems.*>>
+         <<issues.*>>
+      </checkedout>
+
+  NOTE: Not all table fields are available, pleasee see GetPendingIssues for a list of available fields.
+
 =cut
 
 sub IssueSlip {
     my ($branch, $borrowernumber, $quickslip) = @_;
 
-#   return unless ( C4::Context->boolean_preference('printcirculationslips') );
+    # FIXME Check callers before removing this statement
+    #return unless $borrowernumber;
 
-    my $now       = POSIX::strftime("%Y-%m-%d", localtime);
+    my @issues = @{ GetPendingIssues($borrowernumber) };
 
-    my $issueslist = GetPendingIssues($borrowernumber);
-    foreach my $it (@$issueslist){
-        if ((substr $it->{'issuedate'}, 0, 10) eq $now) {
-            $it->{'now'} = 1;
-        }
-        elsif ((substr $it->{'date_due'}, 0, 10) le $now) {
-            $it->{'overdue'} = 1;
+    for my $issue (@issues) {
+        $issue->{date_due} = $issue->{date_due_sql};
+        if ($quickslip) {
+            my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
+            if ( substr( $issue->{issuedate}, 0, 10 ) eq $today
+                or substr( $issue->{lastreneweddate}, 0, 10 ) eq $today ) {
+                  $issue->{now} = 1;
+            };
         }
-
-        $it->{'date_due'}=format_date($it->{'date_due'});
     }
-    my @issues = sort { $b->{'timestamp'} <=> $a->{'timestamp'} } @$issueslist;
+
+    # Sort on timestamp then on issuedate (useful for tests and could be if modified in a batch
+    @issues = sort {
+        my $s = $b->{timestamp} <=> $a->{timestamp};
+        $s == 0 ?
+             $b->{issuedate} <=> $a->{issuedate} : $s;
+    } @issues;
 
     my ($letter_code, %repeat);
     if ( $quickslip ) {
         $letter_code = 'ISSUEQSLIP';
         %repeat =  (
             'checkedout' => [ map {
-                'biblio' => $_,
-                'items'  => $_,
-                'issues' => $_,
+                'biblio'       => $_,
+                'items'        => $_,
+                'biblioitems'  => $_,
+                'issues'       => $_,
             }, grep { $_->{'now'} } @issues ],
         );
     }
@@ -2351,21 +2406,23 @@ sub IssueSlip {
         $letter_code = 'ISSUESLIP';
         %repeat =  (
             'checkedout' => [ map {
-                'biblio' => $_,
-                'items'  => $_,
-                'issues' => $_,
+                'biblio'       => $_,
+                'items'        => $_,
+                'biblioitems'  => $_,
+                'issues'       => $_,
             }, grep { !$_->{'overdue'} } @issues ],
 
             'overdue' => [ map {
-                'biblio' => $_,
-                'items'  => $_,
-                'issues' => $_,
+                'biblio'       => $_,
+                'items'        => $_,
+                'biblioitems'  => $_,
+                'issues'       => $_,
             }, grep { $_->{'overdue'} } @issues ],
 
             'news' => [ map {
                 $_->{'timestamp'} = $_->{'newdate'};
                 { opac_news => $_ }
-            } @{ GetNewsToDisplay("slip") } ],
+            } @{ GetNewsToDisplay("slip",$branch) } ],
         );
     }
 
@@ -2408,6 +2465,10 @@ sub GetBorrowersWithEmail {
     return @result;
 }
 
+=head2 AddMember_Opac
+
+=cut
+
 sub AddMember_Opac {
     my ( %borrower ) = @_;
 
@@ -2425,6 +2486,116 @@ sub AddMember_Opac {
     return ( $borrowernumber, $password );
 }
 
+=head2 AddEnrolmentFeeIfNeeded
+
+    AddEnrolmentFeeIfNeeded( $borrower->{categorycode}, $borrower->{borrowernumber} );
+
+Add enrolment fee for a patron if needed.
+
+=cut
+
+sub AddEnrolmentFeeIfNeeded {
+    my ( $categorycode, $borrowernumber ) = @_;
+    # check for enrollment fee & add it if needed
+    my $dbh = C4::Context->dbh;
+    my $sth = $dbh->prepare(q{
+        SELECT enrolmentfee
+        FROM categories
+        WHERE categorycode=?
+    });
+    $sth->execute( $categorycode );
+    if ( $sth->err ) {
+        warn sprintf('Database returned the following error: %s', $sth->errstr);
+        return;
+    }
+    my ($enrolmentfee) = $sth->fetchrow;
+    if ($enrolmentfee && $enrolmentfee > 0) {
+        # insert fee in patron debts
+        C4::Accounts::manualinvoice( $borrowernumber, '', '', 'A', $enrolmentfee );
+    }
+}
+
+=head2 HasOverdues
+
+=cut
+
+sub HasOverdues {
+    my ( $borrowernumber ) = @_;
+
+    my $sql = "SELECT COUNT(*) FROM issues WHERE date_due < NOW() AND borrowernumber = ?";
+    my $sth = C4::Context->dbh->prepare( $sql );
+    $sth->execute( $borrowernumber );
+    my ( $count ) = $sth->fetchrow_array();
+
+    return $count;
+}
+
+=head2 DeleteExpiredOpacRegistrations
+
+    Delete accounts that haven't been upgraded from the 'temporary' category
+    Returns the number of removed patrons
+
+=cut
+
+sub DeleteExpiredOpacRegistrations {
+
+    my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
+    my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
+
+    return 0 if not $category_code or not defined $delay or $delay eq q||;
+
+    my $query = qq|
+SELECT borrowernumber
+FROM borrowers
+WHERE categorycode = ? AND DATEDIFF( NOW(), dateenrolled ) > ? |;
+
+    my $dbh = C4::Context->dbh;
+    my $sth = $dbh->prepare($query);
+    $sth->execute( $category_code, $delay );
+    my $cnt=0;
+    while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
+        DelMember($borrowernumber);
+        $cnt++;
+    }
+    return $cnt;
+}
+
+=head2 DeleteUnverifiedOpacRegistrations
+
+    Delete all unverified self registrations in borrower_modifications,
+    older than the specified number of days.
+
+=cut
+
+sub DeleteUnverifiedOpacRegistrations {
+    my ( $days ) = @_;
+    my $dbh = C4::Context->dbh;
+    my $sql=qq|
+DELETE FROM borrower_modifications
+WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
+    my $cnt=$dbh->do($sql, undef, ($days) );
+    return $cnt eq '0E0'? 0: $cnt;
+}
+
+sub GetOverduesForPatron {
+    my ( $borrowernumber ) = @_;
+
+    my $sql = "
+        SELECT *
+        FROM issues, items, biblio, biblioitems
+        WHERE items.itemnumber=issues.itemnumber
+          AND biblio.biblionumber   = items.biblionumber
+          AND biblio.biblionumber   = biblioitems.biblionumber
+          AND issues.borrowernumber = ?
+          AND date_due < NOW()
+    ";
+
+    my $sth = C4::Context->dbh->prepare( $sql );
+    $sth->execute( $borrowernumber );
+
+    return $sth->fetchall_arrayref({});
+}
+
 END { }    # module clean-up code here (global destructor)
 
 1;