Bug 23156: Add pagination to checkouts in ILS-DI GetPatronInfo service
[koha-equinox.git] / C4 / ILSDI / Services.pm
index ffd4792..049ddcf 100644 (file)
@@ -4,18 +4,18 @@ package C4::ILSDI::Services;
 #
 # 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., 59 Temple Place,
-# Suite 330, Boston, MA  02111-1307 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;
@@ -23,16 +23,24 @@ use warnings;
 use C4::Members;
 use C4::Items;
 use C4::Circulation;
-use C4::Branch;
 use C4::Accounts;
 use C4::Biblio;
-use C4::Reserves;
+use C4::Reserves qw(AddReserve CanBookBeReserved CanItemBeReserved IsAvailableForItemLevelRequest);
 use C4::Context;
 use C4::AuthoritiesMarc;
-use C4::ILSDI::Utility;
 use XML::Simple;
 use HTML::Entities;
-use CGI;
+use CGI qw ( -utf8 );
+use DateTime;
+use C4::Auth;
+use C4::Members::Attributes qw(GetBorrowerAttributes);
+use Koha::DateUtils;
+
+use Koha::Biblios;
+use Koha::Checkouts;
+use Koha::Items;
+use Koha::Libraries;
+use Koha::Patrons;
 
 =head1 NAME
 
@@ -40,60 +48,71 @@ C4::ILS-DI::Services - ILS-DI Services
 
 =head1 DESCRIPTION
 
-       Each function in this module represents an ILS-DI service.
-       They all takes a CGI instance as argument and most of them return a 
-       hashref that will be printed by XML::Simple in opac/ilsdi.pl
+Each function in this module represents an ILS-DI service.
+They all takes a CGI instance as argument and most of them return a
+hashref that will be printed by XML::Simple in opac/ilsdi.pl
 
 =head1 SYNOPSIS
 
-       use C4::ILSDI::Services;
-       use XML::Simple;
-       use CGI;
+    use C4::ILSDI::Services;
+    use XML::Simple;
+    use CGI qw ( -utf8 );
 
-       my $cgi = new CGI;
+    my $cgi = new CGI;
 
-       $out = LookupPatron($cgi);
+    $out = LookupPatron($cgi);
 
-       print CGI::header('text/xml');
-       print XMLout($out,
-               noattr => 1, 
-               noescape => 1,
-               nosort => 1,
-               xmldecl => '<?xml version="1.0" encoding="ISO-8859-1" ?>', 
-               RootName => 'LookupPatron', 
-               SuppressEmpty => 1);
+    print CGI::header('text/xml');
+    print XMLout($out,
+        noattr => 1,
+        noescape => 1,
+        nosort => 1,
+                xmldecl => '<?xml version="1.0" encoding="UTF-8" ?>',
+        RootName => 'LookupPatron',
+        SuppressEmpty => 1);
 
 =cut
 
+=head1 FUNCTIONS
+
 =head2 GetAvailability
-    
-       Given a set of biblionumbers or itemnumbers, returns a list with 
-       availability of the items associated with the identifiers.
-       
-       Parameters :
-
-       - id (Required)
-               list of either biblionumbers or itemnumbers
-       - id_type (Required)
-               defines the type of record identifier being used in the request, 
-               possible values:
-                       - bib
-                       - item
-       - return_type (Optional)
-               requests a particular level of detail in reporting availability, 
-               possible values:
-                       - bib
-                       - item
-       - return_fmt (Optional)
-               requests a particular format or set of formats in reporting 
-               availability 
+
+Given a set of biblionumbers or itemnumbers, returns a list with
+availability of the items associated with the identifiers.
+
+Parameters:
+
+=head3 id (Required)
+
+list of either biblionumbers or itemnumbers
+
+=head3 id_type (Required)
+
+defines the type of record identifier being used in the request,
+possible values:
+
+  - bib
+  - item
+
+=head3 return_type (Optional)
+
+requests a particular level of detail in reporting availability,
+possible values:
+
+  - bib
+  - item
+
+=head3 return_fmt (Optional)
+
+requests a particular format or set of formats in reporting
+availability
 
 =cut
 
 sub GetAvailability {
     my ($cgi) = @_;
 
-    my $out = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n";
+    my $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
     $out .= "<dlf:collection\n";
     $out .= "  xmlns:dlf=\"http://diglib.org/ilsdi/1.1\"\n";
     $out .= "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
@@ -102,7 +121,7 @@ sub GetAvailability {
 
     foreach my $id ( split( / /, $cgi->param('id') ) ) {
         if ( $cgi->param('id_type') eq "item" ) {
-            my ( $biblionumber, $status, $msg, $location ) = Availability($id);
+            my ( $biblionumber, $status, $msg, $location ) = _availability($id);
 
             $out .= "  <dlf:record>\n";
             $out .= "    <dlf:bibliographic id=\"" . ( $biblionumber || $id ) . "\" />\n";
@@ -120,21 +139,32 @@ sub GetAvailability {
         } else {
             my $status;
             my $msg;
-            my $biblioitem = ( GetBiblioItemByBiblioNumber( $id, undef ) )[0];
-            if ($biblioitem) {
-
+            my $items = Koha::Items->search({ biblionumber => $id });
+            if ($items->count) {
+                # Open XML
+                $out .= "  <dlf:record>\n";
+                $out .= "    <dlf:bibliographic id=\"" .$id. "\" />\n";
+                $out .= "    <dlf:items>\n";
+                # We loop over the items to clean them
+                while ( my $item = $items->next ) {
+                    my $itemnumber = $item->itemnumber;
+                    my ( $biblionumber, $status, $msg, $location ) = _availability($itemnumber);
+                    $out .= "      <dlf:item id=\"" . $itemnumber . "\">\n";
+                    $out .= "        <dlf:simpleavailability>\n";
+                    $out .= "          <dlf:identifier>" . $itemnumber . "</dlf:identifier>\n";
+                    $out .= "          <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
+                    if ($msg)      { $out .= "          <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n"; }
+                    if ($location) { $out .= "          <dlf:location>" . $location . "</dlf:location>\n"; }
+                    $out .= "        </dlf:simpleavailability>\n";
+                    $out .= "      </dlf:item>\n";
+                }
+                # Close XML
+                $out .= "    </dlf:items>\n";
+                $out .= "  </dlf:record>\n";
             } else {
                 $status = "unknown";
                 $msg    = "Error: could not retrieve availability for this ID";
             }
-            $out .= "  <dlf:record>\n";
-            $out .= "    <dlf:bibliographic id=\"" . $id . "\" />\n";
-            $out .= "    <dlf:simpleavailability>\n";
-            $out .= "      <dlf:identifier>" . $id . "</dlf:identifier>\n";
-            $out .= "      <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
-            $out .= "      <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n";
-            $out .= "    </dlf:simpleavailability>\n";
-            $out .= "  </dlf:record>\n";
         }
     }
     $out .= "</dlf:collection>\n";
@@ -143,25 +173,26 @@ sub GetAvailability {
 }
 
 =head2 GetRecords
-    
-       Given a list of biblionumbers, returns a list of record objects that 
-       contain bibliographic information, as well as associated holdings and item
-       information. The caller may request a specific metadata schema for the 
-       record objects to be returned.
-       This function behaves similarly to HarvestBibliographicRecords and 
-       HarvestExpandedRecords in Data Aggregation, but allows quick, real time 
-       lookup by bibliographic identifier.
-
-       You can use OAI-PMH ListRecords instead of this service.
-       
-       Parameters:
-
-       - id (Required)
-               list of system record identifiers
-       - id_type (Optional)
-               Defines the metadata schema in which the records are returned, 
-               possible values:
-                       - MARCXML
+
+Given a list of biblionumbers, returns a list of record objects that
+contain bibliographic information, as well as associated holdings and item
+information. The caller may request a specific metadata schema for the
+record objects to be returned.
+
+This function behaves similarly to HarvestBibliographicRecords and
+HarvestExpandedRecords in Data Aggregation, but allows quick, real time
+lookup by bibliographic identifier.
+
+You can use OAI-PMH ListRecords instead of this service.
+
+Parameters:
+
+  - id (Required)
+    list of system record identifiers
+  - id_type (Optional)
+    Defines the metadata schema in which the records are returned,
+    possible values:
+        - MARCXML
 
 =cut
 
@@ -170,7 +201,7 @@ sub GetRecords {
 
     # Check if the schema is supported. For now, GetRecords only supports MARCXML
     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
-        return { message => 'UnsupportedSchema' };
+        return { code => 'UnsupportedSchema' };
     }
 
     my @records;
@@ -179,41 +210,59 @@ sub GetRecords {
     foreach my $biblionumber ( split( / /, $cgi->param('id') ) ) {
 
         # Get the biblioitem from the biblionumber
-        my $biblioitem = ( GetBiblioItemByBiblioNumber( $biblionumber, undef ) )[0];
-        if ( not $biblioitem->{'biblionumber'} ) {
-            $biblioitem = "RecordNotFound";
+        my $biblio = Koha::Biblios->find( $biblionumber );
+        unless ( $biblio ) {
+            push @records, { code => "RecordNotFound" };
+            next;
         }
 
-        # We don't want MARC to be displayed
-        delete $biblioitem->{'marc'};
+        my $biblioitem = $biblio->biblioitem->unblessed;
 
-        # nor the XML declaration of MARCXML
-        $biblioitem->{'marcxml'} =~ s/<\?xml version="1.0" encoding="UTF-8"\?>//go;
+        my $embed_items = 1;
+        my $record = GetMarcBiblio({
+            biblionumber => $biblionumber,
+            embed_items  => $embed_items });
+        if ($record) {
+            $biblioitem->{marcxml} = $record->as_xml_record();
+        }
 
         # Get most of the needed data
         my $biblioitemnumber = $biblioitem->{'biblioitemnumber'};
-        my @reserves         = GetReservesFromBiblionumber( $biblionumber, undef, undef );
+        my $holds  = $biblio->current_holds->unblessed;
         my $issues           = GetBiblioIssues($biblionumber);
-        my $items            = GetItemsByBiblioitemnumber($biblioitemnumber);
+        my @items            = $biblio->items->as_list;
+
+        $biblioitem->{items}->{item} = [];
 
         # We loop over the items to clean them
-        foreach my $item (@$items) {
+        foreach my $item (@items) {
+            my %item = %{ $item->unblessed };
 
             # This hides additionnal XML subfields, we don't need these info
-            delete $item->{'more_subfields_xml'};
+            delete $item{'more_subfields_xml'};
 
             # Display branch names instead of branch codes
-            $item->{'homebranchname'}    = GetBranchName( $item->{'homebranch'} );
-            $item->{'holdingbranchname'} = GetBranchName( $item->{'holdingbranch'} );
+            my $home_library    = $item->home_branch;
+            my $holding_library = $item->holding_branch;
+            $item{'homebranchname'}    = $home_library    ? $home_library->branchname    : '';
+            $item{'holdingbranchname'} = $holding_library ? $holding_library->branchname : '';
+
+            my $transfer = $item->get_transfer;
+            if ($transfer) {
+                $item{transfer} = {
+                    datesent => $transfer->datesent,
+                    frombranch => $transfer->frombranch,
+                    tobranch => $transfer->tobranch,
+                };
+            }
+
+            push @{ $biblioitem->{items}->{item} }, \%item;
         }
 
         # Hashref building...
-        $biblioitem->{'items'}->{'item'}       = $items;
-        $biblioitem->{'reserves'}->{'reserve'} = $reserves[1];
+        $biblioitem->{'reserves'}->{'reserve'} = $holds;
         $biblioitem->{'issues'}->{'issue'}     = $issues;
 
-        map { $biblioitem->{$_} = encode_entities( $biblioitem->{$_}, '&' ) } grep( !/marcxml/, keys %$biblioitem );
-
         push @records, $biblioitem;
     }
 
@@ -221,18 +270,18 @@ sub GetRecords {
 }
 
 =head2 GetAuthorityRecords
-    
-       Given a list of authority record identifiers, returns a list of record 
-       objects that contain the authority records. The function user may request 
-       a specific metadata schema for the record objects.
 
-       Parameters:
+Given a list of authority record identifiers, returns a list of record
+objects that contain the authority records. The function user may request
+a specific metadata schema for the record objects.
+
+Parameters:
 
-       - id (Required)
-           list of authority record identifiers
-       - schema (Optional)
-           specifies the metadata schema of records to be returned, possible values:
-                 - MARCXML
+  - id (Required)
+    list of authority record identifiers
+  - schema (Optional)
+    specifies the metadata schema of records to be returned, possible values:
+      - MARCXML
 
 =cut
 
@@ -241,107 +290,117 @@ sub GetAuthorityRecords {
 
     # If the user asks for an unsupported schema, return an error code
     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
-        return { message => 'UnsupportedSchema' };
+        return { code => 'UnsupportedSchema' };
     }
 
-    my $records;
+    my @records;
 
     # Let's loop over the authority IDs
     foreach my $authid ( split( / /, $cgi->param('id') ) ) {
 
         # Get the record as XML string, or error code
-        my $record = GetAuthorityXML($authid) || "<record>RecordNotFound</record>";
-        $record =~ s/<\?xml version="1.0" encoding="UTF-8"\?>//go;
-        $records .= $record;
+        push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' };
     }
 
-    return $records;
+    return { record => \@records };
 }
 
 =head2 LookupPatron
-    
-       Looks up a patron in the ILS by an identifier, and returns the borrowernumber.
-       
-       Parameters:
-
-       - id (Required)
-               an identifier used to look up the patron in Koha
-       - id_type (Optional)
-               the type of the identifier, possible values:
-                       - cardnumber
-                       - firstname
-                       - userid
-                       - borrowernumber
+
+Looks up a patron in the ILS by an identifier, and returns the borrowernumber.
+
+Parameters:
+
+  - id (Required)
+    an identifier used to look up the patron in Koha
+  - id_type (Optional)
+    the type of the identifier, possible values:
+    - cardnumber
+    - userid
+        - email
+    - borrowernumber
+    - firstname
+        - surname
 
 =cut
 
 sub LookupPatron {
     my ($cgi) = @_;
 
-    # Get the borrower...
-    my $borrower = GetMember( $cgi->param('id'), $cgi->param('id_type') );
-    if ( not $borrower->{'borrowernumber'} ) {
+    my $id      = $cgi->param('id');
+    if(!$id) {
         return { message => 'PatronNotFound' };
     }
 
-    # Build the hashref
-    my $patron->{'id'} = $borrower->{'borrowernumber'};
+    my $patrons;
+    my $passed_id_type = $cgi->param('id_type');
+    if($passed_id_type) {
+        $patrons = Koha::Patrons->search( { $passed_id_type => $id } );
+    } else {
+        foreach my $id_type ('cardnumber', 'userid', 'email', 'borrowernumber',
+                     'surname', 'firstname') {
+            $patrons = Koha::Patrons->search( { $id_type => $id } );
+            last if($patrons->count);
+        }
+    }
+    unless ( $patrons->count ) {
+        return { message => 'PatronNotFound' };
+    }
 
-    # ...and return his ID
-    return $patron;
+    return { id => $patrons->next->borrowernumber };
 }
 
 =head2 AuthenticatePatron
 
-       Authenticates a user's login credentials and returns the identifier for 
-       the patron.
-       
-       Parameters:
+Authenticates a user's login credentials and returns the identifier for
+the patron.
+
+Parameters:
+
+  - username (Required)
+    user's login identifier (userid or cardnumber)
+  - password (Required)
+    user's password
 
-       - username (Required)
-               user's login identifier
-       - password (Required)
-               user's password
-               
 =cut
 
 sub AuthenticatePatron {
     my ($cgi) = @_;
-
-    # Check if borrower exists, using a C4::ILSDI::Utility function...
-    if ( not( BorrowerExists( $cgi->param('username'), $cgi->param('password') ) ) ) {
-        return { message => 'PatronNotFound' };
+    my $username = $cgi->param('username');
+    my $password = $cgi->param('password');
+    my ($status, $cardnumber, $userid) = C4::Auth::checkpw( C4::Context->dbh, $username, $password );
+    if ( $status ) {
+        # Get the borrower
+        my $patron = Koha::Patrons->find( { userid => $userid } );
+        return { id => $patron->borrowernumber };
+    }
+    else {
+        return { code => 'PatronNotFound' };
     }
-
-    # Get the borrower
-    my $borrower = GetMember( $cgi->param('username'), "userid" );
-
-    # Build the hashref
-    my $patron->{'id'} = $borrower->{'borrowernumber'};
-
-    # ... and return his ID
-    return $patron;
 }
 
 =head2 GetPatronInfo
 
-       Returns specified information about the patron, based on options in the 
-       request. This function can optionally return patron's contact information, 
-       fine information, hold request information, and loan information.
-       
-       Parameters:
-
-       - patron_id (Required)
-               the borrowernumber
-       - show_contact (Optional, default 1)
-               whether or not to return patron's contact information in the response
-       - show_fines (Optional, default 0)
-               whether or not to return fine information in the response
-       - show_holds (Optional, default 0)
-               whether or not to return hold request information in the response
-       - show_loans (Optional, default 0)
-               whether or not to return loan information request information in the response 
-               
+Returns specified information about the patron, based on options in the
+request. This function can optionally return patron's contact information,
+fine information, hold request information, and loan information.
+
+Parameters:
+
+  - patron_id (Required)
+    the borrowernumber
+  - show_contact (Optional, default 1)
+    whether or not to return patron's contact information in the response
+  - show_fines (Optional, default 0)
+    whether or not to return fine information in the response
+  - show_holds (Optional, default 0)
+    whether or not to return hold request information in the response
+  - show_loans (Optional, default 0)
+    whether or not to return loan information request information in the response
+  - show_attributes (Optional, default 0)
+    whether or not to return additional patron attributes, when enabled the attributes
+    are limited to those marked as opac visible only.
+
 =cut
 
 sub GetPatronInfo {
@@ -349,20 +408,19 @@ sub GetPatronInfo {
 
     # Get Member details
     my $borrowernumber = $cgi->param('patron_id');
-    my $borrower = GetMemberDetails( $borrowernumber, undef );
-    if ( not $borrower->{'borrowernumber'} ) {
-        return { message => 'PatronNotFound' };
-    }
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    return { code => 'PatronNotFound' } unless $patron;
 
     # Cleaning the borrower hashref
-    $borrower->{'charges'}    = $borrower->{'flags'}->{'CHARGES'}->{'amount'};
-    $borrower->{'branchname'} = GetBranchName( $borrower->{'branchcode'} );
-    delete $borrower->{'flags'};
+    my $borrower = $patron->unblessed;
+    $borrower->{charges} = sprintf "%.02f", $patron->account->non_issues_charges; # FIXME Formatting should not be done here
+    my $library = Koha::Libraries->find( $borrower->{branchcode} );
+    $borrower->{'branchname'} = $library ? $library->branchname : '';
     delete $borrower->{'userid'};
     delete $borrower->{'password'};
 
     # Contact fields management
-    if ( $cgi->param('show_contact') eq "0" ) {
+    if ( defined $cgi->param('show_contact') && $cgi->param('show_contact') eq "0" ) {
 
         # Define contact fields
         my @contactfields = (
@@ -379,55 +437,92 @@ sub GetPatronInfo {
     }
 
     # Fines management
-    if ( $cgi->param('show_fines') eq "1" ) {
-        my @charges;
-        for ( my $i = 1 ; my @charge = getcharges( $borrowernumber, undef, $i ) ; $i++ ) {
-            push( @charges, @charge );
-        }
-        $borrower->{'fines'}->{'fine'} = \@charges;
+    if ( $cgi->param('show_fines') && $cgi->param('show_fines') eq "1" ) {
+        $borrower->{fines}{fine} = $patron->account->lines->unblessed;
     }
 
     # Reserves management
-    if ( $cgi->param('show_holds') eq "1" ) {
+    if ( $cgi->param('show_holds') && $cgi->param('show_holds') eq "1" ) {
 
         # Get borrower's reserves
-        my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
-        foreach my $reserve (@reserves) {
+        my $holds = $patron->holds;
+        while ( my $hold = $holds->next ) {
 
+            my ( $item, $biblio, $biblioitem ) = ( {}, {}, {} );
             # Get additional informations
-            my $item = GetBiblioFromItemNumber( $reserve->{'itemnumber'}, undef );
-            my $branchname = GetBranchName( $reserve->{'branchcode'} );
-
-            # Remove unwanted fields
-            delete $item->{'marc'};
-            delete $item->{'marcxml'};
-            delete $item->{'more_subfields_xml'};
+            if ( $hold->itemnumber ) {    # item level holds
+                $item       = Koha::Items->find( $hold->itemnumber );
+                $biblio     = $item->biblio;
+                $biblioitem = $biblio->biblioitem;
+
+                # Remove unwanted fields
+                $item = $item->unblessed;
+                delete $item->{more_subfields_xml};
+                $biblio     = $biblio->unblessed;
+                $biblioitem = $biblioitem->unblessed;
+            }
 
             # Add additional fields
-            $reserve->{'item'}       = $item;
-            $reserve->{'branchname'} = $branchname;
-            $reserve->{'title'}      = ( GetBiblio( $reserve->{'biblionumber'} ) )[1]->{'title'};
+            my $unblessed_hold = $hold->unblessed;
+            $unblessed_hold->{item}       = { %$item, %$biblio, %$biblioitem };
+            my $library = Koha::Libraries->find( $hold->branchcode );
+            my $branchname = $library ? $library->branchname : '';
+            $unblessed_hold->{branchname} = $branchname;
+            $biblio = Koha::Biblios->find( $hold->biblionumber ); # Should be $hold->get_biblio
+            $unblessed_hold->{title} = $biblio ? $biblio->title : ''; # Just in case, but should not be needed
+
+            push @{ $borrower->{holds}{hold} }, $unblessed_hold;
+
         }
-        $borrower->{'holds'}->{'hold'} = \@reserves;
     }
 
     # Issues management
-    if ( $cgi->param('show_loans') eq "1" ) {
-        my $issues = GetPendingIssues($borrowernumber);
-        $borrower->{'loans'}->{'loan'} = $issues;
+    if ( $cgi->param('show_loans') && $cgi->param('show_loans') eq "1" ) {
+        my $per_page = $cgi->param('loans_per_page');
+        my $page = $cgi->param('loans_page');
+
+        my $pending_checkouts = $patron->pending_checkouts;
+
+        if ($page || $per_page) {
+            $page ||= 1;
+            $per_page ||= 10;
+            $borrower->{total_loans} = $pending_checkouts->count();
+            $pending_checkouts = $pending_checkouts->search(undef, {
+                rows => $per_page,
+                page => $page,
+            });
+        }
+
+        my @checkouts;
+        while ( my $c = $pending_checkouts->next ) {
+            # FIXME We should only retrieve what is needed in the template
+            my $issue = $c->unblessed_all_relateds;
+            delete $issue->{'more_subfields_xml'};
+            push @checkouts, $issue
+        }
+        $borrower->{'loans'}->{'loan'} = \@checkouts;
+    }
+
+    my $show_attributes = $cgi->param('show_attributes');
+    if ( $show_attributes && $show_attributes eq "1" ) {
+        my $attrs = GetBorrowerAttributes( $borrowernumber, 1 );
+        $borrower->{'attributes'} = $attrs;
     }
 
+    # Add is expired information
+    $borrower->{'is_expired'} = $patron->is_expired ? 1 : 0;
+
     return $borrower;
 }
 
 =head2 GetPatronStatus
 
-       Returns a patron's status information.
-       
-       Parameters:
+Returns a patron's status information.
+
+Parameters:
 
-       - patron_id (Required)
-               the borrower ID
+  - patron_id (Required)
+    the borrower ID
 
 =cut
 
@@ -436,31 +531,29 @@ sub GetPatronStatus {
 
     # Get Member details
     my $borrowernumber = $cgi->param('patron_id');
-    my $borrower = GetMemberDetails( $borrowernumber, undef );
-    if ( not $borrower->{'borrowernumber'} ) {
-        return { message => 'PatronNotFound' };
-    }
-
-    # Hashref building
-    my $patron;
-    $patron->{'type'}   = $borrower->{'categorycode'};
-    $patron->{'status'} = 0;                             #TODO
-    $patron->{'expiry'} = $borrower->{'dateexpiry'};
-
-    return $patron;
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    return { code => 'PatronNotFound' } unless $patron;
+
+    # Return the results
+    return {
+        type   => $patron->categorycode,
+        status => 0, # TODO
+        expiry => $patron->dateexpiry,
+    };
 }
 
 =head2 GetServices
 
-       Returns information about the services available on a particular item for 
-       a particular patron.
-       
-       Parameters:
+Returns information about the services available on a particular item for
+a particular patron.
+
+Parameters:
+
+  - patron_id (Required)
+    a borrowernumber
+  - item_id (Required)
+    an itemnumber
 
-       - patron_id (Required)
-               a borrowernumber
-       - item_id (Required)
-               an itemnumber
 =cut
 
 sub GetServices {
@@ -468,36 +561,33 @@ sub GetServices {
 
     # Get the member, or return an error code if not found
     my $borrowernumber = $cgi->param('patron_id');
-    my $borrower = GetMemberDetails( $borrowernumber, undef );
-    if ( not $borrower->{'borrowernumber'} ) {
-        return { message => 'PatronNotFound' };
-    }
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    return { code => 'PatronNotFound' } unless $patron;
 
+    my $borrower = $patron->unblessed;
     # Get the item, or return an error code if not found
     my $itemnumber = $cgi->param('item_id');
-    my $item = GetItem( $itemnumber, undef, undef );
-    if ( not $item->{'itemnumber'} ) {
-        return { message => 'RecordNotFound' };
-    }
+    my $item = Koha::Items->find($itemnumber);
+    return { code => 'RecordNotFound' } unless $item;
 
     my @availablefor;
 
     # Reserve level management
-    my $biblionumber = $item->{'biblionumber'};
+    my $biblionumber = $item->biblionumber;
     my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
-    if ($canbookbereserved) {
+    if ($canbookbereserved->{status} eq 'OK') {
         push @availablefor, 'title level hold';
-        my $canitembereserved = IsAvailableForItemLevelRequest($itemnumber);
+        my $canitembereserved = IsAvailableForItemLevelRequest($item, $patron);
         if ($canitembereserved) {
             push @availablefor, 'item level hold';
         }
     }
 
     # Reserve cancellation management
-    my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
+    my $holds = $patron->holds;
     my @reserveditems;
-    foreach my $reserve (@reserves) {
-        push @reserveditems, $reserve->{'itemnumber'};
+    while ( my $hold = $holds->next ) { # FIXME This could be improved
+        push @reserveditems, $hold->itemnumber;
     }
     if ( grep { $itemnumber eq $_ } @reserveditems ) {
         push @availablefor, 'hold cancellation';
@@ -510,10 +600,10 @@ sub GetServices {
     }
 
     # Issuing management
-    my $barcode = $item->{'barcode'} || '';
+    my $barcode = $item->barcode || '';
     $barcode = barcodedecode($barcode) if ( $barcode && C4::Context->preference('itemBarcodeInputFilter') );
     if ($barcode) {
-        my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower, $barcode );
+        my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $barcode );
 
         # TODO push @availablefor, 'loan';
     }
@@ -526,16 +616,16 @@ sub GetServices {
 
 =head2 RenewLoan
 
-       Extends the due date for a borrower's existing issue.
-       
-       Parameters:
+Extends the due date for a borrower's existing issue.
+
+Parameters:
 
-       - patron_id (Required)
-               a borrowernumber
-       - item_id (Required)
-               an itemnumber
-       - desired_due_date (Required)
-               the date the patron would like the item returned by 
+  - patron_id (Required)
+    a borrowernumber
+  - item_id (Required)
+    an itemnumber
+  - desired_due_date (Required)
+    the date the patron would like the item returned by
 
 =cut
 
@@ -544,28 +634,25 @@ sub RenewLoan {
 
     # Get borrower infos or return an error code
     my $borrowernumber = $cgi->param('patron_id');
-    my $borrower = GetMemberDetails( $borrowernumber, undef );
-    if ( not $borrower->{'borrowernumber'} ) {
-        return { message => 'PatronNotFound' };
-    }
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    return { code => 'PatronNotFound' } unless $patron;
 
     # Get the item, or return an error code
     my $itemnumber = $cgi->param('item_id');
-    my $item = GetItem( $itemnumber, undef, undef );
-    if ( not $item->{'itemnumber'} ) {
-        return { message => 'RecordNotFound' };
-    }
+    my $item = Koha::Items->find($itemnumber);
+    return { code => 'RecordNotFound' } unless $item;
 
     # Add renewal if possible
     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
     if ( $renewal[0] ) { AddRenewal( $borrowernumber, $itemnumber ); }
 
-    my $issue = GetItemIssue($itemnumber);
+    my $issue = $item->checkout;
+    return unless $issue; # FIXME should be handled
 
     # Hashref building
     my $out;
-    $out->{'renewals'} = $issue->{'renewals'};
-    $out->{'date_due'} = $issue->{'date_due'};
+    $out->{'renewals'} = $issue->renewals;
+    $out->{date_due}   = dt_from_string($issue->date_due)->strftime('%Y-%m-%d %H:%M');
     $out->{'success'}  = $renewal[0];
     $out->{'error'}    = $renewal[1];
 
@@ -574,22 +661,22 @@ sub RenewLoan {
 
 =head2 HoldTitle
 
-       Creates, for a borrower, a biblio-level hold reserve.
-       
-       Parameters:
-
-       - patron_id (Required)
-               a borrowernumber
-       - bib_id (Required)
-               a biblionumber
-       - request_location (Required)
-               IP address where the end user request is being placed
-       - pickup_location (Optional)
-               a branch code indicating the location to which to deliver the item for pickup
-       - needed_before_date (Optional)
-               date after which hold request is no longer needed
-       - pickup_expiry_date (Optional)
-               date after which item returned to shelf if item is not picked up 
+Creates, for a borrower, a biblio-level hold reserve.
+
+Parameters:
+
+  - patron_id (Required)
+    a borrowernumber
+  - bib_id (Required)
+    a biblionumber
+  - request_location (Required)
+    IP address where the end user request is being placed
+  - pickup_location (Optional)
+    a branch code indicating the location to which to deliver the item for pickup
+  - needed_before_date (Optional)
+    date after which hold request is no longer needed
+  - pickup_expiry_date (Optional)
+    date after which item returned to shelf if item is not picked up
 
 =cut
 
@@ -598,46 +685,58 @@ sub HoldTitle {
 
     # Get the borrower or return an error code
     my $borrowernumber = $cgi->param('patron_id');
-    my $borrower = GetMemberDetails( $borrowernumber, undef );
-    if ( not $borrower->{'borrowernumber'} ) {
-        return { message => 'PatronNotFound' };
-    }
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    return { code => 'PatronNotFound' } unless $patron;
 
     # Get the biblio record, or return an error code
     my $biblionumber = $cgi->param('bib_id');
-    my ( $count, $biblio ) = GetBiblio($biblionumber);
-    if ( not $biblio->{'biblionumber'} ) {
-        return { message => 'RecordNotFound' };
+    my $biblio = Koha::Biblios->find( $biblionumber );
+    return { code => 'RecordNotFound' } unless $biblio;
+
+    my @hostitems = get_hostitemnumbers_of($biblionumber);
+    my @itemnumbers;
+    if (@hostitems){
+        push(@itemnumbers, @hostitems);
     }
-    my $title = $biblio->{'title'};
 
-    # Check if the biblio can be reserved
-    my $canbereserved = CanBookBeReserved( $borrower, $biblionumber );
-    if ( not $canbereserved ) {
-        return { message => 'NotHoldable' };
+    my $items = Koha::Items->search({ -or => { biblionumber => $biblionumber, itemnumber => { in => \@itemnumbers } } });
+
+    unless ( $items->count ) {
+        return { code => 'NoItems' };
     }
 
+    my $title = $biblio ? $biblio->title : '';
+
+    # Check if the biblio can be reserved
+    my $code = CanBookBeReserved( $borrowernumber, $biblionumber )->{status};
+    return { code => $code } unless ( $code eq 'OK' );
+
     my $branch;
 
     # Pickup branch management
     if ( $cgi->param('pickup_location') ) {
         $branch = $cgi->param('pickup_location');
-        my $branches = GetBranches();
-        if ( not $branches->{$branch} ) {
-            return { message => 'LocationNotFound' };
-        }
-    } else {    # if user provide no branch, use his own
-        $branch = $borrower->{'branchcode'};
+        return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
+    } else { # if the request provide no branch, use the borrower's branch
+        $branch = $patron->branchcode;
     }
 
+    my $destination = Koha::Libraries->find($branch);
+    return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location;
+    return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred({ to => $destination });
+
     # Add the reserve
-    #          $branch, $borrowernumber, $biblionumber, $constraint, $bibitems,  $priority, $notes, $title, $checkitem,  $found
-    AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, 0, undef, $title, undef, undef );
+    #    $branch,    $borrowernumber, $biblionumber,
+    #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
+    #    $title,      $checkitem, $found
+    my $priority= C4::Reserves::CalculatePriority( $biblionumber );
+    AddReserve( $branch, $borrowernumber, $biblionumber, undef, $priority, undef, undef, undef, $title, undef, undef );
 
     # Hashref building
     my $out;
     $out->{'title'}           = $title;
-    $out->{'pickup_location'} = GetBranchName($branch);
+    my $library = Koha::Libraries->find( $branch );
+    $out->{'pickup_location'} = $library ? $library->branchname : '';
 
     # TODO $out->{'date_available'}  = '';
 
@@ -646,23 +745,23 @@ sub HoldTitle {
 
 =head2 HoldItem
 
-       Creates, for a borrower, an item-level hold request on a specific item of 
-       a bibliographic record in Koha.
+Creates, for a borrower, an item-level hold request on a specific item of
+a bibliographic record in Koha.
 
-       Parameters:
+Parameters:
 
-       - patron_id (Required)
-               a borrowernumber
-       - bib_id (Required)
-               a biblionumber
-       - item_id (Required)
-               an itemnumber
-       - pickup_location (Optional)
-               a branch code indicating the location to which to deliver the item for pickup
-       - needed_before_date (Optional)
-               date after which hold request is no longer needed
-       - pickup_expiry_date (Optional)
-               date after which item returned to shelf if item is not picked up 
+  - patron_id (Required)
+    a borrowernumber
+  - bib_id (Required)
+    a biblionumber
+  - item_id (Required)
+    an itemnumber
+  - pickup_location (Optional)
+    a branch code indicating the location to which to deliver the item for pickup
+  - needed_before_date (Optional)
+    date after which hold request is no longer needed
+  - pickup_expiry_date (Optional)
+    date after which item returned to shelf if item is not picked up
 
 =cut
 
@@ -671,67 +770,48 @@ sub HoldItem {
 
     # Get the borrower or return an error code
     my $borrowernumber = $cgi->param('patron_id');
-    my $borrower = GetMemberDetails( $borrowernumber, undef );
-    if ( not $borrower->{'borrowernumber'} ) {
-        return { message => 'PatronNotFound' };
-    }
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    return { code => 'PatronNotFound' } unless $patron;
 
     # Get the biblio or return an error code
     my $biblionumber = $cgi->param('bib_id');
-    my ( $count, $biblio ) = GetBiblio($biblionumber);
-    if ( not $biblio->{'biblionumber'} ) {
-        return { message => 'RecordNotFound' };
-    }
-    my $title = $biblio->{'title'};
+    my $biblio = Koha::Biblios->find( $biblionumber );
+    return { code => 'RecordNotFound' } unless $biblio;
+
+    my $title = $biblio ? $biblio->title : '';
 
     # Get the item or return an error code
     my $itemnumber = $cgi->param('item_id');
-    my $item = GetItem( $itemnumber, undef, undef );
-    if ( not $item->{'itemnumber'} ) {
-        return { message => 'RecordNotFound' };
-    }
+    my $item = Koha::Items->find($itemnumber);
+    return { code => 'RecordNotFound' } unless $item;
 
-    # if the biblio does not match the item, return an error code
-    if ( $item->{'biblionumber'} ne $biblio->{'biblionumber'} ) {
-        return { message => 'RecordNotFound' };
-    }
-
-    # Check for item disponibility
-    my $canitembereserved = IsAvailableForItemLevelRequest($itemnumber);
-    my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
-    if ( ( not $canbookbereserved ) or not($canitembereserved) ) {
-        return { message => 'NotHoldable' };
-    }
-
-    my $branch;
+    # If the biblio does not match the item, return an error code
+    return { code => 'RecordNotFound' } if $item->biblionumber ne $biblio->biblionumber;
 
     # Pickup branch management
+    my $branch;
     if ( $cgi->param('pickup_location') ) {
         $branch = $cgi->param('pickup_location');
-        my $branches = GetBranches();
-        if ( not $branches->{$branch} ) {
-            return { message => 'LocationNotFound' };
-        }
-    } else {    # if user provide no branch, use his own
-        $branch = $borrower->{'branchcode'};
+        return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
+    } else { # if the request provide no branch, use the borrower's branch
+        $branch = $patron->branchcode;
     }
 
-    my $rank;
-    my $found;
-
-    # Get rank and found
-    $rank = '0' unless C4::Context->preference('ReservesNeedReturns');
-    if ( $item->{'holdingbranch'} eq $branch ) {
-        $found = 'W' unless C4::Context->preference('ReservesNeedReturns');
-    }
+    # Check for item disponibility
+    my $canitembereserved = C4::Reserves::CanItemBeReserved( $borrowernumber, $itemnumber, $branch )->{status};
+    return { code => $canitembereserved } unless $canitembereserved eq 'OK';
 
     # Add the reserve
-    #          $branch, $borrowernumber, $biblionumber, $constraint, $bibitems,  $priority, $notes, $title, $checkitem,  $found
-    AddReserve( $branch, $borrowernumber, $biblionumber, 'a', undef, $rank, undef, $title, $itemnumber, $found );
+    #    $branch,    $borrowernumber, $biblionumber,
+    #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
+    #    $title,      $checkitem, $found
+    my $priority= C4::Reserves::CalculatePriority( $biblionumber );
+    AddReserve( $branch, $borrowernumber, $biblionumber, undef, $priority, undef, undef, undef, $title, $itemnumber, undef );
 
     # Hashref building
     my $out;
-    $out->{'pickup_location'} = GetBranchName($branch);
+    my $library = Koha::Libraries->find( $branch );
+    $out->{'pickup_location'} = $library ? $library->branchname : '';
 
     # TODO $out->{'date_available'} = '';
 
@@ -740,14 +820,14 @@ sub HoldItem {
 
 =head2 CancelHold
 
-       Cancels an active reserve request for the borrower.
-       
-       Parameters:
+Cancels an active reserve request for the borrower.
 
-       - patron_id (Required)
-               a borrowernumber
-       - item_id (Required)
-               an itemnumber 
+Parameters:
+
+  - patron_id (Required)
+        a borrowernumber
+  - item_id (Required)
+        a reserve_id
 
 =cut
 
@@ -756,37 +836,53 @@ sub CancelHold {
 
     # Get the borrower or return an error code
     my $borrowernumber = $cgi->param('patron_id');
-    my $borrower = GetMemberDetails( $borrowernumber, undef );
-    if ( not $borrower->{'borrowernumber'} ) {
-        return { message => 'PatronNotFound' };
-    }
+    my $patron = Koha::Patrons->find( $borrowernumber );
+    return { code => 'PatronNotFound' } unless $patron;
 
-    # Get the item or return an error code
-    my $itemnumber = $cgi->param('item_id');
-    my $item = GetItem( $itemnumber, undef, undef );
-    if ( not $item->{'itemnumber'} ) {
-        return { message => 'RecordNotFound' };
-    }
+    # Get the reserve or return an error code
+    my $reserve_id = $cgi->param('item_id');
+    my $hold = Koha::Holds->find( $reserve_id );
+    return { code => 'RecordNotFound' } unless $hold;
+    return { code => 'RecordNotFound' } unless ($hold->borrowernumber == $borrowernumber);
 
-    # Get borrower's reserves
-    my @reserves = GetReservesFromBorrowernumber( $borrowernumber, undef );
-    my @reserveditems;
+    $hold->cancel;
 
-    # ...and loop over it to build an array of reserved itemnumbers
-    foreach my $reserve (@reserves) {
-        push @reserveditems, $reserve->{'itemnumber'};
-    }
+    return { code => 'Canceled' };
+}
 
-    # if the item was not reserved by the borrower, returns an error code
-    if ( not grep { $itemnumber eq $_ } @reserveditems ) {
-        return { message => 'NotCanceled' };
-    }
+=head2 _availability
 
-    # Cancel the reserve
-    CancelReserve( $itemnumber, undef, $borrowernumber );
+Returns, for an itemnumber, an array containing availability information.
 
-    return { message => 'Canceled' };
+ my ($biblionumber, $status, $msg, $location) = _availability($id);
 
+=cut
+
+sub _availability {
+    my ($itemnumber) = @_;
+    my $item = Koha::Items->find($itemnumber);
+
+    unless ( $item ) {
+        return ( undef, 'unknown', 'Error: could not retrieve availability for this ID', undef );
+    }
+
+    my $biblionumber = $item->biblioitemnumber;
+    my $library = Koha::Libraries->find( $item->holdingbranch );
+    my $location = $library ? $library->branchname : '';
+
+    if ( $item->notforloan ) {
+        return ( $biblionumber, 'not available', 'Not for loan', $location );
+    } elsif ( $item->onloan ) {
+        return ( $biblionumber, 'not available', 'Checked out', $location );
+    } elsif ( $item->itemlost ) {
+        return ( $biblionumber, 'not available', 'Item lost', $location );
+    } elsif ( $item->withdrawn ) {
+        return ( $biblionumber, 'not available', 'Item withdrawn', $location );
+    } elsif ( $item->damaged ) {
+        return ( $biblionumber, 'not available', 'Item damaged', $location );
+    } else {
+        return ( $biblionumber, 'available', undef, $location );
+    }
 }
 
 1;