Bug 15344: Remove some other calls of GetMemberDetails from pl scripts
[koha.git] / C4 / Members.pm
index 9f6659f..fafa02f 100644 (file)
@@ -23,7 +23,6 @@ package C4::Members;
 use strict;
 #use warnings; FIXME - Bug 2505
 use C4::Context;
-use C4::Dates qw(format_date_in_iso format_date);
 use String::Random qw( random_string );
 use Date::Calc qw/Today Add_Delta_YM check_date Date_to_Days/;
 use C4::Log; # logaction
@@ -32,7 +31,6 @@ 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 UpdateBorrowerAttribute);
 use C4::NewsChannels; #get slip news
 use DateTime;
@@ -42,9 +40,10 @@ use Koha::Borrower::Debarments qw(IsDebarred);
 use Text::Unaccent qw( unac_string );
 use Koha::AuthUtils qw(hash_password);
 use Koha::Database;
-use Module::Load;
-if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
-    load Koha::NorwegianPatronDB, qw( NLUpdateHashedPIN NLEncryptPIN NLSync );
+
+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);
@@ -98,6 +97,7 @@ BEGIN {
         &GetBorrowersWithIssuesHistoryOlderThan
 
         &GetExpiryDate
+        &GetUpcomingMembershipExpires
 
         &AddMessage
         &DeleteMessage
@@ -108,6 +108,7 @@ BEGIN {
         GetBorrowersWithEmail
 
         HasOverdues
+        GetOverduesForPatron
     );
 
     #Modify data
@@ -136,8 +137,6 @@ BEGIN {
         &checkuserpassword
         &Check_Userid
         &Generate_Userid
-        &fixEthnicity
-        &ethnicitycategories
         &fixup_cardnumber
         &checkcardnumber
     );
@@ -157,139 +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, $not_attributes ) = @_;
-
-    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 && !$not_attributes ) {
-        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("IndependentBranches") ) { # && !$showallbranches){
-        if ( my $userenv = C4::Context->userenv ) {
-            my $branch =  $userenv->{'branch'};
-            if ( !C4::Context->IsSuperLibrarian() && $branch ){
-                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);
@@ -376,16 +242,6 @@ 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')
@@ -649,25 +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 or a manual debarment, in which case
+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
 
@@ -791,14 +641,28 @@ sub ModMember {
         } else {
             if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
                 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
-                NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
+                Koha::NorwegianPatronDB::NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
             }
             $data{password} = hash_password($data{password});
         }
     }
     my $old_categorycode = GetBorrowerCategorycode( $data{borrowernumber} );
-    my $execute_success=UpdateInTable("borrowers",\%data);
-    if ($execute_success) { # only proceed if the update was a success
+
+    # 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
@@ -810,7 +674,9 @@ sub ModMember {
 
         # If the patron changes to a category with enrollment fee, we add a fee
         if ( $data{categorycode} and $data{categorycode} ne $old_categorycode ) {
-            AddEnrolmentFeeIfNeeded( $data{categorycode}, $data{borrowernumber} );
+            if ( C4::Context->preference('FeeOnChangePatronCategory') ) {
+                AddEnrolmentFeeIfNeeded( $data{categorycode}, $data{borrowernumber} );
+            }
         }
 
         # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
@@ -829,7 +695,7 @@ sub ModMember {
             # Set the value of 'sync'
             $borrowersync->update( { 'sync' => $data{'sync'} } );
             # Try to do the live sync
-            NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
+            Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
         }
 
         logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog");
@@ -857,6 +723,7 @@ 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'} )
@@ -864,17 +731,15 @@ sub AddMember {
 
     # 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 =
-      Koha::Database->new()->schema()->resultset('Category')
-      ->find( $data{'categorycode'} );
+    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
@@ -885,7 +750,18 @@ sub AddMember {
 
     # create a disabled account if no password provided
     $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!';
-    $data{'borrowernumber'}=InsertInTable("borrowers",\%data);
+
+    # 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
@@ -895,7 +771,7 @@ sub AddMember {
             'synctype'       => 'norwegianpatrondb',
             'sync'           => 1,
             'syncstatus'     => 'new',
-            'hashed_pin'     => NLEncryptPIN( $plain_text_password ),
+            'hashed_pin'     => Koha::NorwegianPatronDB::NLEncryptPIN( $plain_text_password ),
         });
     }
 
@@ -904,7 +780,7 @@ sub AddMember {
 
     AddEnrolmentFeeIfNeeded( $data{categorycode}, $data{borrowernumber} );
 
-    return $data{'borrowernumber'};
+    return $data{borrowernumber};
 }
 
 =head2 Check_Userid
@@ -1199,6 +1075,8 @@ sub GetPendingIssues {
         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_sql} = $_->{date_due};
         # FIXME no need to have this value
@@ -1285,7 +1163,7 @@ sub GetMemberAccountRecords {
                         SELECT * 
                         FROM accountlines 
                         WHERE borrowernumber=?);
-    $strsth.=" ORDER BY date desc,timestamp DESC";
+    $strsth.=" ORDER BY accountlines_id desc";
     my $sth= $dbh->prepare( $strsth );
     $sth->execute( $borrowernumber );
 
@@ -1598,6 +1476,30 @@ sub GetExpiryDate {
     }
 }
 
+=head2 GetUpcomingMembershipExpires
+
+  my $upcoming_mem_expires = GetUpcomingMembershipExpires();
+
+=cut
+
+sub GetUpcomingMembershipExpires {
+    my $dbh = C4::Context->dbh;
+    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
 
   ($codes_arrayref, $labels_hashref) = &GetborCatFromCatType();
@@ -1726,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);
@@ -1952,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);
@@ -1967,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);
@@ -2366,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;
@@ -2472,6 +2360,8 @@ sub DeleteMessage {
          <<issues.*>>
       </checkedout>
 
+  NOTE: Not all table fields are available, pleasee see GetPendingIssues for a list of available fields.
+
 =cut
 
 sub IssueSlip {
@@ -2575,6 +2465,10 @@ sub GetBorrowersWithEmail {
     return @result;
 }
 
+=head2 AddMember_Opac
+
+=cut
+
 sub AddMember_Opac {
     my ( %borrower ) = @_;
 
@@ -2621,6 +2515,10 @@ sub AddEnrolmentFeeIfNeeded {
     }
 }
 
+=head2 HasOverdues
+
+=cut
+
 sub HasOverdues {
     my ( $borrowernumber ) = @_;
 
@@ -2632,6 +2530,72 @@ sub HasOverdues {
     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;