From c0ad30cce6bca7f5b1275411c68891945608d804 Mon Sep 17 00:00:00 2001
From: Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
Date: Fri, 3 Feb 2017 16:58:35 +0100
Subject: [PATCH] Bug 7317: ILLModule

* Koha/Illrequest.pm
* Koha/Illrequest/Config.pm
* Koha/Illrequestattribute.pm
* Koha/Illrequestattributes.pm
* Koha/Illrequests.pm
* Koha/REST/V1/Illrequests.pm
* ill/ill-requests.pl
* api/v1/swagger/paths.json
* api/v1/swagger/paths/illrequests.json
* etc/koha-conf.xml
* koha-tmpl/intranet-tmpl/prog/css/staff-global.css
* koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc
* koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc
* koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc
* koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt
* koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt
* koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc
* koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt
* koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt
* koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
* koha-tmpl/opac-tmpl/bootstrap/less/opac.less
* opac/opac-illrequests.pl
* t/db_dependent/Illrequest.t
* t/db_dependent/Illrequest/Config.t
* t/db_dependent/Illrequest/Dummy.t
* t/db_dependent/api/v1/illrequests.t

Signed-off-by: Magnus Enger <magnus@libriotech.no>
---
 Koha/Illrequest.pm                                 | 855 +++++++++++++++++++++
 Koha/Illrequest/Backend/Dummy/Base.pm              | 576 ++++++++++++++
 Koha/Illrequest/Config.pm                          | 389 ++++++++++
 Koha/Illrequestattribute.pm                        |  51 ++
 Koha/Illrequestattributes.pm                       |  55 ++
 Koha/Illrequests.pm                                |  99 +++
 Koha/REST/V1/Illrequests.pm                        |  85 ++
 api/v1/swagger/paths.json                          |   3 +
 api/v1/swagger/paths/illrequests.json              |  98 +++
 etc/koha-conf.xml                                  |  21 +
 ill/ill-requests.pl                                | 238 ++++++
 koha-tmpl/intranet-tmpl/prog/css/staff-global.css  |  77 ++
 .../intranet-tmpl/prog/en/includes/circ-menu.inc   |   3 +
 .../intranet-tmpl/prog/en/includes/ill-toolbar.inc |  24 +
 .../intranet-tmpl/prog/en/includes/permissions.inc |   1 +
 .../prog/en/modules/ill/ill-requests.tt            | 487 ++++++++++++
 .../intranet-tmpl/prog/en/modules/intranet-main.tt |   5 +
 .../opac-tmpl/bootstrap/en/includes/usermenu.inc   |   9 +
 .../bootstrap/en/modules/opac-illrequests.tt       | 219 ++++++
 .../bootstrap/en/modules/opac-results-grouped.tt   |  30 +-
 .../opac-tmpl/bootstrap/en/modules/opac-results.tt |  33 +-
 koha-tmpl/opac-tmpl/bootstrap/less/opac.less       |  38 +
 opac/opac-illrequests.pl                           | 129 ++++
 t/db_dependent/Illrequest.t                        | 544 +++++++++++++
 t/db_dependent/Illrequest/Config.t                 | 344 +++++++++
 t/db_dependent/Illrequest/Dummy.t                  | 378 +++++++++
 t/db_dependent/api/v1/illrequests.t                | 136 ++++
 27 files changed, 4910 insertions(+), 17 deletions(-)
 create mode 100644 Koha/Illrequest.pm
 create mode 100644 Koha/Illrequest/Backend/Dummy/Base.pm
 create mode 100644 Koha/Illrequest/Config.pm
 create mode 100644 Koha/Illrequestattribute.pm
 create mode 100644 Koha/Illrequestattributes.pm
 create mode 100644 Koha/Illrequests.pm
 create mode 100644 Koha/REST/V1/Illrequests.pm
 create mode 100644 api/v1/swagger/paths/illrequests.json
 create mode 100755 ill/ill-requests.pl
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt
 create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt
 create mode 100755 opac/opac-illrequests.pl
 create mode 100644 t/db_dependent/Illrequest.t
 create mode 100644 t/db_dependent/Illrequest/Config.t
 create mode 100644 t/db_dependent/Illrequest/Dummy.t
 create mode 100644 t/db_dependent/api/v1/illrequests.t

diff --git a/Koha/Illrequest.pm b/Koha/Illrequest.pm
new file mode 100644
index 0000000..d975ede
--- /dev/null
+++ b/Koha/Illrequest.pm
@@ -0,0 +1,855 @@
+package Koha::Illrequest;
+
+# Copyright PTFS Europe 2016
+#
+# 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 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.
+#
+# 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.
+
+# use Modern::Perl;
+
+use Clone 'clone';
+use File::Basename qw/basename/;
+use Koha::Database;
+use Koha::Email;
+use Koha::Illrequestattributes;
+use Koha::Patron;
+use Mail::Sendmail;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::Illrequest - Koha Illrequest Object class
+
+=head1 (Re)Design
+
+An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
+of related Illrequestattributes.
+
+The former encapsulates the basic necessary information that any ILL requires
+to be usable in Koha.  The latter is a set of additional properties used by
+one of the backends.
+
+The former subsumes the legacy "Status" object.  The latter remains
+encapsulated in the "Record" object.
+
+TODO:
+
+- Anything invoking the ->status method; annotated with:
+  + # Old use of ->status !
+
+=head1 API
+
+=head2 Backend API Response Principles
+
+All methods should return a hashref in the following format:
+
+=item * error
+
+This should be set to 1 if an error was encountered.
+
+=item * status
+
+The status should be a string from the list of statuses detailed below.
+
+=item * message
+
+The message is a free text field that can be passed on to the end user.
+
+=item * value
+
+The value returned by the method.
+
+=over
+
+=head2 Interface Status Messages
+
+=over
+
+=item * branch_address_incomplete
+
+An interface request has determined branch address details are incomplete.
+
+=item * cancel_success
+
+The interface's cancel_request method was successful in cancelling the
+Illrequest using the API.
+
+=item * cancel_fail
+
+The interface's cancel_request method failed to cancel the Illrequest using
+the API.
+
+=item * unavailable
+
+The interface's request method returned saying that the desired item is not
+available for request.
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequest';
+}
+
+# XXX: Method to expose DBIx accessor for illrequestattributes relationship
+sub illrequestattributes {
+    my ( $self ) = @_;
+    return Koha::Illrequestattributes->_new_from_dbic(
+        scalar $self->_result->illrequestattributes
+    );
+}
+
+# XXX: Method to expose DBIx accessor for borrower relationship
+sub patron {
+    my ( $self ) = @_;
+    return Koha::Patron->_new_from_dbic(
+        scalar $self->_result->borrowernumber
+    );
+}
+
+sub load_backend {
+    my ( $self, $backend_id ) = @_;
+
+    my @raw = qw/Koha Illbackends/; # Base Path
+
+    my $backend_name = $backend_id || $self->backend;
+    $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
+    $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
+    require $location;
+    $self->{_my_backend} = $backend_class->new({ config => $self->_config });
+    return $self;
+}
+
+=head3 _backend
+
+    my $backend = $abstract->_backend($new_backend);
+    my $backend = $abstract->_backend;
+
+Getter/Setter for our API object.
+
+=cut
+
+sub _backend {
+    my ( $self, $backend ) = @_;
+    $self->{_my_backend} = $backend if ( $backend );
+    # Dynamically load our backend object, as late as possible.
+    $self->load_backend unless ( $self->{_my_backend} );
+    return $self->{_my_backend};
+}
+
+=head3 _config
+
+    my $config = $abstract->_config($config);
+    my $config = $abstract->_config;
+
+Getter/Setter for our config object.
+
+=cut
+
+sub _config {
+    my ( $self, $config ) = @_;
+    $self->{_my_config} = $config if ( $config );
+    # Load our config object, as late as possible.
+    unless ( $self->{_my_config} ) {
+        $self->{_my_config} = Koha::Illrequest::Config->new;
+    }
+    return $self->{_my_config};
+}
+
+=head3 metadata
+
+=cut
+
+sub metadata {
+    my ( $self ) = @_;
+    return $self->_backend->metadata($self);
+}
+
+=head3 _core_status_graph
+
+=cut
+
+sub _core_status_graph {
+    my ( $self ) = @_;
+    return {
+        NEW => {
+            prev_actions => [ ],                           # Actions containing buttons
+                                                           # leading to this status
+            id             => 'NEW',                       # ID of this status
+            name           => 'New request',               # UI name of this status
+            ui_method_name => 'New request',               # UI name of method leading
+                                                           # to this status
+            method         => 'create',                    # method to this status
+            next_actions   => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
+                                                           # requests with this status
+            ui_method_icon => 'fa-plus',                   # UI Style class
+        },
+        REQ => {
+            prev_actions   => [ 'NEW', 'REQREV', 'QUEUED' ],
+            id             => 'REQ',
+            name           => 'Requested',
+            ui_method_name => 'Confirm request',
+            method         => 'confirm',
+            next_actions   => [ 'REQREV', 'CANCREQ' ],
+            ui_method_icon => 'fa-check',
+        },
+        GENREQ => {
+            prev_actions   => [ 'NEW', 'REQREV' ],
+            id             => 'GENREQ',
+            name           => 'Requested from partners',
+            ui_method_name => 'Place request with partners',
+            method         => 'generic_confirm',
+            next_actions   => [ 'COMP' ],
+            ui_method_icon => 'fa-send-o',
+        },
+        REQREV => {
+            prev_actions   => [ 'CANCREQ', 'REQ' ],
+            id             => 'REQREV',
+            name           => 'Request reverted',
+            ui_method_name => 'Revert Request',
+            method         => 'cancel',
+            next_actions   => [ 'REQ', 'GENREQ', 'KILL' ],
+            ui_method_icon => 'fa-times',
+        },
+        QUEUED => {
+            prev_actions   => [ ],
+            id             => 'QUEUED',
+            name           => 'Queued request',
+            ui_method_name => 0,
+            method         => 0,
+            next_actions   => [ 'REQ', 'KILL' ],
+            ui_method_icon => 0,
+        },
+        CANCREQ => {
+            prev_actions   => [ 'REQ' ],
+            id             => 'CANCREQ',
+            name           => 'Cancellation requested',
+            ui_method_name => 0,
+            method         => 0,
+            next_actions   => [ 'REQREV' ],
+            ui_method_icon => 0,
+        },
+        COMP => {
+            prev_actions   => [ 'REQ' ],
+            id             => 'COMP',
+            name           => 'Completed',
+            ui_method_name => 0,
+            method         => 0,
+            next_actions   => [ ],
+            ui_method_icon => 0,
+        },
+        KILL => {
+            prev_actions   => [ 'QUEUED', 'REQREV', 'NEW' ],
+            id             => 'KILL',
+            name           => 0,
+            ui_method_name => 'Delete request',
+            method         => 'delete',
+            next_actions   => [ ],
+            ui_method_icon => 'fa-trash',
+        },
+    };
+}
+
+sub _status_graph_union {
+    my ( $self, $core_status_graph, $backend_status_graph ) = @_;
+    # Create new status graph with:
+    # - all core_status_graph
+    # - for-each each backend_status_graph
+    #   + add to new status graph
+    #   + for each core prev_action:
+    #     * locate core_status
+    #     * update next_actions with additional next action.
+    #   + for each core next_action:
+    #     * locate core_status
+    #     * update prev_actions with additional prev action
+
+    my @core_status_ids = keys %{$core_status_graph};
+    my $status_graph = clone($core_status_graph);
+
+    foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
+        $backend_status = $backend_status_graph->{$backend_status_key};
+        # Add to new status graph
+        $status_graph->{$backend_status_key} = $backend_status;
+        # Update all core methods' next_actions.
+        foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
+            if ( grep $prev_action, @core_status_ids ) {
+                my @next_actions =
+                     @{$status_graph->{$prev_action}->{next_actions}};
+                push @next_actions, $backend_status_key;
+                $status_graph->{$prev_action}->{next_actions}
+                    = \@next_actions;
+            }
+        }
+        # Update all core methods' prev_actions
+        foreach my $next_action ( @{$backend_status->{next_actions}} ) {
+            if ( grep $next_action, @core_status_ids ) {
+                my @prev_actions =
+                     @{$status_graph->{$next_action}->{prev_actions}};
+                push @prev_actions, $backend_status_key;
+                $status_graph->{$next_action}->{prev_actions}
+                    = \@prev_actions;
+            }
+        }
+    }
+
+    return $status_graph;
+}
+
+### Core API methods
+
+=head3 capabilities
+
+    my $capabilities = $illrequest->capabilities;
+
+Return a hashref mapping methods to operation names supported by the queried
+backend.
+
+Example return value:
+
+    { create => "Create Request", confirm => "Progress Request" }
+
+=cut
+
+sub capabilities {
+    my ( $self, $status ) = @_;
+    # Generate up to date status_graph
+    my $status_graph = $self->_status_graph_union(
+        $self->_core_status_graph,
+        $self->_backend->status_graph({
+            request => $self,
+            other   => {}
+        })
+    );
+    # Extract available actions from graph.
+    return $status_graph->{$status} if $status;
+    # Or return entire graph.
+    return $status_graph;
+}
+
+=head3 custom_capability
+
+Return the result of invoking $CANDIDATE on this request's backend with
+$PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
+
+=cut
+
+sub custom_capability {
+    my ( $self, $candidate, $params ) = @_;
+    foreach my $capability ( values $self->capabilities ) {
+        if ( $candidate eq $capability->{method} ) {
+            my $response =
+                $self->_backend->$candidate({
+                    request    => $self,
+                    other      => $params,
+                });
+            return $self->expandTemplate($response);
+        }
+    }
+    return 0;
+}
+
+sub available_backends {
+    my ( $self ) = @_;
+    my $backend_dir = $self->_config->backend_dir;
+    my @backends = ();
+    my @backends = <$backend_dir/*> if ( $backend_dir );
+    my @backends = map { basename($_) } @backends;
+    return \@backends;
+}
+
+sub available_actions {
+    my ( $self ) = @_;
+    my $current_action = $self->capabilities($self->status);
+    my @available_actions = map { $self->capabilities($_) }
+        @{$current_action->{next_actions}};
+    return \@available_actions;
+}
+
+sub backend_confirm {
+    my ( $self, $params ) = @_;
+
+    # The backend handles setting of mandatory fields in the commit stage:
+    # - orderid
+    # - accessurl, cost (if available).
+    my $response = $self->_backend->confirm({
+            request    => $self,
+            other      => $params,
+        });
+    return $self->expandTemplate($response);
+}
+
+sub backend_update_status {
+    my ( $self, $params ) = @_;
+    return $self->expandTemplate($self->_backend->update_status($params));
+}
+
+=head3 backend_cancel
+
+    my $ILLResponse = $illRequest->backend_cancel;
+
+The standard interface method allowing for request cancellation.
+
+=cut
+
+sub backend_cancel {
+    my ( $self, $params ) = @_;
+
+    my $result = $self->_backend->cancel({
+        request => $self,
+        other => $params
+    });
+
+    return $self->expandTemplate($result);
+}
+
+=head3 backend_renew
+
+    my $renew_response = $illRequest->backend_renew;
+
+The standard interface method allowing for request renewal queries.
+
+=cut
+
+sub backend_renew {
+    my ( $self ) = @_;
+    return $self->expandTemplate(
+        $self->_backend->renew({
+            request    => $self,
+        })
+    );
+}
+
+=head3 backend_create
+
+    my $create_response = $abstractILL->backend_create($params);
+
+Return an array of Record objects created by querying our backend with
+a Search query.
+
+In the context of the other ILL methods, this is a special method: we only
+pass it $params, as it does not yet have any other data associated with it.
+
+=cut
+
+sub backend_create {
+    my ( $self, $params ) = @_;
+
+    # First perform API action, then...
+    my $args = {
+        request => $self,
+        other   => $params,
+    };
+    my $result = $self->_backend->create($args);
+
+    # ... simple case: we're not at 'commit' stage.
+    my $stage = $result->{stage};
+    return $self->expandTemplate($result)
+        unless ( 'commit' eq $stage );
+
+    # ... complex case: commit!
+
+    # Do we still have space for an ILL or should we queue?
+    my $permitted = $self->check_limits(
+        { patron => $self->patron }, { librarycode => $self->branchcode }
+    );
+
+    # Now augment our committed request.
+
+    $result->{permitted} = $permitted;             # Queue request?
+
+    # This involves...
+
+    # ...Updating status!
+    $self->status('QUEUED')->store unless ( $permitted );
+
+    # FIXME: Fix Unmediated ILLs!
+    # Handle Unmediated ILLs
+    # if ( C4::Context->preference("UnmediatedILL") && $permitted
+    #       # XXX: Why && result manual?
+    #       && $result->{manual} ) {
+    #     # FIXME: Also carry out privilege checks
+    #     my ( $response, $new_rq ) = $self->place_request; # WTF?
+    #     if ( $response ) {
+    #         $result->{value}->{request} = $new_rq;
+    #         return $result;
+    #     } else {
+    #         die "Placing the request failed.";
+    #     }
+    # } else {
+    #     $result->{value}->{request} = $request;
+    #     return $result;
+    # }
+
+    return $self->expandTemplate($result);
+}
+
+=head3 expandTemplate
+
+    my $params = $abstract->expandTemplate($params);
+
+Return a version of $PARAMS augmented with our required template path.
+
+=cut
+
+sub expandTemplate {
+    my ( $self, $params ) = @_;
+    my $backend = $self->_backend->name;
+    # Generate path to file to load
+    my $backend_dir = $self->_config->backend_dir;
+    my $backend_tmpl = join "/", $backend_dir, $backend;
+    my $intra_tmpl =  join "/", $backend_tmpl, "intra-includes",
+        $params->{method} . ".inc";
+    my $opac_tmpl =  join "/", $backend_tmpl, "opac-includes",
+        $params->{method} . ".inc";
+    # Set files to load
+    $params->{template} = $intra_tmpl;
+    $params->{opac_template} = $opac_tmpl;
+    return $params;
+}
+
+#### Abstract Imports
+
+=head3 getCensorNotesStaff
+
+    my $bool = $abstract->getCensorNotesStaff;
+
+Return a boolean indicating whether we should be censoring staff notes or not,
+as determined by our configuration file.
+
+=cut
+
+sub getCensorNotesStaff {
+    my ( $self ) = @_;
+    my $censorship = $self->_config->censorship;
+    return $censorship->{censor_notes_staff};
+}
+
+=head3 getDisplayReplyDate
+
+    my 0 = $abstract->getDisplayReplyDate;
+
+Return a 0 if we want to hide it or 1 if not.
+
+=cut
+
+sub getDisplayReplyDate {
+    my ( $self ) = @_;
+    my $censorship = $self->_config->censorship;
+    # If censor is yes, don't display and vice versa.
+    return ( $censorship->{censor_reply_date} ) ? 0 : 1;
+}
+
+=head3 getLimits
+
+    my $limit_rules = $abstract->getLimits( {
+        type  => 'brw_cat' | 'branch',
+        value => $value
+    } );
+
+Return the ILL limit rules for the supplied combination of type / value.
+
+As the config may have no rules for this particular type / value combination,
+or for the default, we must define fall-back values here.
+
+=cut
+
+# FIXME: Needs unit tests!
+sub getLimits {
+    my ( $self, $params ) = @_;
+    my $limits = $self->_config->getLimitRules($params->{type});
+
+    return $limits->{$params->{value}}
+        || $limits->{default}
+        || { count => -1, method => 'active' };
+}
+
+=head3 getPrefix
+
+    my $prefix = $abstract->getPrefix( {
+        brw_cat => $brw_cat,
+        branch  => $branch_code,
+    } );
+
+Return the ILL prefix as defined by our $params: either per borrower category,
+per branch or the default.
+
+=cut
+
+sub getPrefix {
+    my ( $self, $params ) = @_;
+    my $brn_prefixes = $self->_config->getPrefixes('branch');
+    my $brw_prefixes = $self->_config->getPrefixes('brw_cat');
+
+    return $brw_prefixes->{$params->{brw_cat}}
+        || $brn_prefixes->{$params->{branch}}
+        || $brw_prefixes->{default}
+        || "";                  # "the empty prefix"
+}
+
+#### Illrequests Imports
+
+=head3 check_limits
+
+    my $ok = $illRequests->check_limits( {
+        borrower   => $borrower,
+        branchcode => 'branchcode' | undef,
+    } );
+
+Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
+see whether we are still able to place ILLs.
+
+LimitRules are derived from koha-conf.xml:
+ + default limit counts, and counting method
+ + branch specific limit counts & counting method
+ + borrower category specific limit counts & counting method
+ + err on the side of caution: a counting fail will cause fail, even if
+   the other counts passes.
+
+=cut
+
+# FIXME: Needs unit tests!
+sub check_limits {
+    my ( $self, $params ) = @_;
+    my $patron     = $params->{patron};
+    my $branchcode = $params->{librarycode} || $patron->branchcode;
+
+    # Establish rules
+    my ( $branch_rules, $brw_rules ) = (
+        $self->getLimits( {
+            type => 'branch',
+            value => $branchcode
+        } ),
+        $self->getLimits( {
+            type => 'brw_cat',
+            value => $patron->categorycode,
+        } ),
+    );
+    # Almost there, but category code didn't quite work.
+    my ( $branch_limit, $brw_limit )
+        = ( $branch_rules->{count}, $brw_rules->{count} );
+    my ( $branch_count, $brw_count ) = (
+        $self->_limit_counter(
+            $branch_rules->{method}, { branch_id => $branchcode }
+        ),
+        $self->_limit_counter(
+            $brw_rules->{method}, { borrower_id => $patron->borrowernumber }
+        ),
+    );
+
+    # Compare and return
+    # A limit of -1 means no limit exists.
+    # We return blocked if either branch limit or brw limit is reached.
+    if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
+             || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
+        return 0;
+    } else {
+        return 1;
+    }
+}
+
+# FIXME: Needs unit tests!
+sub _limit_counter {
+    my ( $self, $method, $target ) = @_;
+
+    # Establish parameters of counts
+    my $where;
+    if ($method && $method eq 'annual') {
+        $where = \"YEAR(placement_date) = YEAR(NOW())";
+    } else {                    # assume 'active'
+        # FIXME: This status list is ugly. There should be a method in config
+        # to return these.
+        $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
+    }
+
+    # Create resultset
+    my $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
+
+    # Fetch counts
+    return $resultset->count;
+}
+
+=head3 requires_moderation
+
+    my $status = $illRequest->requires_moderation;
+
+Return the name of the status if moderation by staff is required; or 0
+otherwise.
+
+=cut
+
+sub requires_moderation {
+    my ( $self ) = @_;
+    my $require_moderation = {
+        'CANCREQ' => 'CANCREQ',
+    };
+    return $require_moderation->{$self->status};
+}
+
+=head3 generic_confirm
+
+    my $stage_summary = $illRequest->generic_confirm;
+
+Handle the generic_confirm extended method.  The first stage involves creating
+a template email for the end user to edit in the browser.  The second stage
+attempts to submit the email.
+
+=cut
+
+sub generic_confirm {
+    my ( $self, $params ) = @_;
+    if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
+        my $draft->{subject} = "ILL Request";
+        $draft->{body} = <<EOF;
+Dear Sir/Madam,
+
+    We would like to request an interlibrary loan for a title matching the
+following description:
+
+EOF
+
+        my $details = $self->metadata;
+        while (my ($title, $value) = each %{$details}) {
+            $draft->{body} .= "  - " . $title . ": " . $value . "\n"
+                if $value;
+        }
+        $draft->{body} .= <<EOF;
+
+Please let us know if you are able to supply this to us.
+
+Kind Regards
+EOF
+
+        my $partners = Koha::Patrons->search({
+            categorycode => $self->_config->partner_code
+        });
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'generic_confirm',
+            stage   => 'draft',
+            value   => {
+                draft    => $draft,
+                partners => $partners,
+            }
+        };
+
+    } elsif ( 'draft' eq $params->{stage} ) {
+        # Create the to header
+        my $to = $params->{partners};
+        $to =~ s/^\x00//;       # Strip leading NULLs
+        $to =~ s/\x00/; /;      # Replace others with '; '
+        die "No target email addresses found. Either select at least one partner or check your ILL partner library records." if ( !$to );
+        # Create the from, replyto and sender headers
+        $branch = Koha::Libraries->find($params->{current_branchcode})
+            || die "Invalid current branchcode. Are you logged in as the database user?";
+        my $from = $branch->branchemail;
+        my $replyto = $branch->branchreplyto || $from;
+        die "Your branch has no email address. Please set it."
+            if ( !$from );
+        # Create the email
+        my $message = Koha::Email->new;
+        my %mail = $message->create_message_headers(
+            {
+                to          => $to,
+                from        => $from,
+                replyto     => $replyto,
+                subject     => Encode::encode( "utf8", $params->{subject} ),
+                message     => Encode::encode( "utf8", $params->{body} ),
+                contenttype => 'text/plain',
+            }
+        );
+        # Send it
+        my $result = sendmail(%mail);
+        if ( $result ) {
+            $self->status("GENREQ")->store;
+            return {
+                error   => 0,
+                status  => '',
+                message => '',
+                method  => 'generic_confirm',
+                stage   => 'commit',
+                next    => 'illview',
+            };
+        } else {
+            return {
+                error   => 1,
+                status  => 'email_failed',
+                message => $Mail::Sendmail::error,
+                method  => 'generic_confirm',
+                stage   => 'draft',
+            };
+        }
+    } else {
+        die "Unknown stage, should not have happened."
+    }
+}
+
+=head3 id_prefix
+
+    my $prefix = $record->id_prefix;
+
+Return the prefix appropriate for the current Illrequest as derived from the
+borrower and branch associated with this request's Status, and the config
+file.
+
+=cut
+
+sub id_prefix {
+    my ( $self ) = @_;
+    # FIXME: can we automatically use borrowernumber as borrower?
+    my $brw = $self->patron;
+    my $brw_cat = "dummy";
+    $brw_cat = $brw->categorycode
+        unless ( 'HASH' eq ref($brw) && $brw->{deleted} );
+    my $prefix = $self->getPrefix( {
+        brw_cat => $brw_cat,
+        branch  => $self->branchcode,
+    } );
+    $prefix .= "-" if ( $prefix );
+    return $prefix;
+}
+
+=head3 _censor
+
+    my $params = $illRequest->_censor($params);
+
+Return $params, modified to reflect our censorship requirements.
+
+=cut
+
+sub _censor {
+    my ( $self, $params ) = @_;
+    $params->{censor_notes_staff} = $self->getCensorNotesStaff
+        if ( $params->{opac} );
+    $params->{display_reply_date} = $self->getDisplayReplyDate;
+
+    return $params;
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequest/Backend/Dummy/Base.pm b/Koha/Illrequest/Backend/Dummy/Base.pm
new file mode 100644
index 0000000..b832bf8
--- /dev/null
+++ b/Koha/Illrequest/Backend/Dummy/Base.pm
@@ -0,0 +1,576 @@
+package Koha::Illrequest::Backend::Dummy::Base;
+
+# Copyright PTFS Europe 2014
+#
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+use DateTime;
+use Koha::Illrequestattribute;
+
+=head1 NAME
+
+Koha::Illrequest::Backend::Dummy - Koha ILL Backend: Dummy
+
+=head1 SYNOPSIS
+
+Koha ILL implementation for the "Dummy" backend.
+
+=head1 DESCRIPTION
+
+=head2 Overview
+
+We will be providing the Abstract interface which requires we implement the
+following methods:
+- create        -> initial placement of the request for an ILL order
+- confirm       -> confirm placement of the ILL order
+- list          -> list all ILL Requests currently placed with the backend
+- renew         -> request a currently borrowed ILL be renewed in the backend
+- update_status -> ILL module update hook: custom actions on status update
+- cancel        -> request an already 'confirm'ed ILL order be cancelled
+- status        -> request the current status of a confirmed ILL order
+
+Each of the above methods will receive the following parameter from
+Illrequest.pm:
+
+  {
+      request    => $request,
+      other      => $other,
+  }
+
+where:
+
+- $REQUEST is the Illrequest object in Koha.  It's associated
+  Illrequestattributes can be accessed through the `illrequestattributes`
+  method.
+- $OTHER is any further data, generally provided through templates .INCs
+
+Each of the above methods should return a hashref of the following format:
+
+    return {
+        error   => 0,
+        # ^------- 0|1 to indicate an error
+        status  => 'result_code',
+        # ^------- Summary of the result of the operation
+        message => 'Human readable message.',
+        # ^------- Message, possibly to be displayed
+        #          Normally messages are derived from status in INCLUDE.
+        #          But can be used to pass API messages to the INCLUDE.
+        method  => 'list',
+        # ^------- Name of the current method invoked.
+        #          Used to load the appropriate INCLUDE.
+        stage   => 'commit',
+        # ^------- The current stage of this method
+        #          Used by INCLUDE to determine HTML to generate.
+        #          'commit' will result in final processing by Illrequest.pm.
+        next    => 'illview'|'illlist',
+        # ^------- When stage is 'commit', should we move on to ILLVIEW the
+        #          current request or ILLLIST all requests.
+        value   => {},
+        # ^------- A hashref containing an arbitrary return value that this
+        #          backend wants to supply to its INCLUDE.
+    };
+
+=head2 On the Dummy backend
+
+The Dummy backend is rather simple, but provides correctly formatted response
+values, that other backends can model themselves after.
+
+The code is not DRY -- primarily so that each method can be looked at in
+isolation rather than having to familiarise oneself with helper procedures.
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 new
+
+  my $backend = Koha::Illrequest::Backend::Dummy->new;
+
+=cut
+
+sub new {
+    # -> instantiate the backend
+    my ( $class ) = @_;
+    my $self = {};
+    bless( $self, $class );
+    return $self;
+}
+
+=head3 _data_store
+
+  my $request = $self->_data_store($id);
+  my $requests = $self->_data_store;
+
+A mock of a data store.  When passed no parameters it returns all entries.
+When passed one it will return the entry matched by its id.
+
+=cut
+
+sub _data_store {
+    my $data = {
+        1234 => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+        },
+        5678 => {
+            id     => 5678,
+            title  => "Interlibrary loans in Koha",
+            author => "A.N. Other",
+        },
+    };
+    # ID search
+    my ( $self, $id ) = @_;
+    return $data->{$id} if $id;
+
+    # Full search
+    my @entries;
+    while ( my ( $k, $v ) = each %{$data} ) {
+        push @entries, $v;
+    }
+    return \@entries;
+}
+
+=head3 create
+
+  my $response = $backend->create({
+      request    => $requestdetails,
+      other      => $other,
+  });
+
+This is the initial creation of the request.  Generally this stage will be
+some form of search with the backend.
+
+By and large we will not have useful $requestdetails (borrowernumber,
+branchcode, status, etc.).
+
+$params is simply an additional slot for any further arbitrary values to pass
+to the backend.
+
+This is an example of a multi-stage method.
+
+=cut
+
+sub create {
+    # -> initial placement of the request for an ILL order
+    my ( $self, $params ) = @_;
+    my $stage = $params->{other}->{stage};
+    if ( !$stage || $stage eq 'init' ) {
+        # We simply need our template .INC to produce a search form.
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'create',
+            stage   => 'search_form',
+            value   => {},
+        };
+    } elsif ( $stage eq 'search_form' ) {
+	# Received search query in 'other'; perform search...
+
+        # No-op on Dummy
+
+        # and return results.
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'create',
+            stage   => 'search_results',
+            value   => {
+                borrowernumber => $params->{other}->{borrowernumber},
+                branchcode     => $params->{other}->{branchcode},
+                medium         => $params->{other}->{medium},
+                candidates     => $self->_data_store,
+            }
+        };
+    } elsif ( $stage eq 'search_results' ) {
+        # We have a selection
+        my $id = $params->{other}->{id};
+
+        # -> select from backend...
+        my $request_details = $self->_data_store($id);
+
+        # ...Populate Illrequest
+        my $request = $params->{request};
+        $request->borrower_id($params->{other}->{borrowernumber});
+        $request->branch_id($params->{other}->{branchcode});
+        $request->medium($params->{other}->{medium});
+        $request->status('NEW');
+        $request->placed(DateTime->now);
+        $request->updated(DateTime->now);
+        $request->store;
+        # ...Populate Illrequestattributes
+        while ( my ( $type, $value ) = each %{$request_details} ) {
+            Koha::Illrequestattribute->new({
+                illrequest_id => $request->illrequest_id,
+                type          => $type,
+                value         => $value,
+            })->store;
+        }
+
+        # -> create response.
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'create',
+            stage   => 'commit',
+            next    => 'illview',
+            value   => $request_details,
+        };
+    } else {
+	# Invalid stage, return error.
+        return {
+            error   => 1,
+            status  => 'unknown_stage',
+            message => '',
+            method  => 'create',
+            stage   => $params->{stage},
+            value   => {},
+        };
+    }
+}
+
+=head3 confirm
+
+  my $response = $backend->confirm({
+      request    => $requestdetails,
+      other      => $other,
+  });
+
+Confirm the placement of the previously "selected" request (by using the
+'create' method).
+
+In this case we will generally use $request.
+This will be supplied at all times through Illrequest.  $other may be supplied
+using templates.
+
+=cut
+
+sub confirm {
+    # -> confirm placement of the ILL order
+    my ( $self, $params ) = @_;
+    # Turn Illrequestattributes into a plain hashref
+    my $value = {};
+    my $attributes = $params->{request}->illrequestattributes;
+    foreach my $attr (@{$attributes->as_list}) {
+        $value->{$attr->type} = $attr->value;
+    };
+    # Submit request to backend...
+
+    # No-op for Dummy
+
+    # ...parse response...
+    $attributes->find_or_create({ type => "status", value => "On order" });
+    my $request = $params->{request};
+    $request->cost("30 GBP");
+    $request->orderid($value->{id});
+    $request->status("REQ");
+    $request->accessurl("URL") if $value->{url};
+    $request->store;
+    $value->{status} = "On order";
+    $value->{cost} = "30 GBP";
+    # ...then return our result:
+    return {
+        error    => 0,
+        status   => '',
+        message  => '',
+        method   => 'confirm',
+        stage    => 'commit',
+        next     => 'illview',
+        value    => $value,
+    };
+}
+
+=head3 list
+
+  my $response = $backend->list({
+      request    => $requestdetails,
+      other      => $other,
+  };
+
+Attempt to get a list of the currently registered requests with the backend.
+
+Parameters are optional for this request.  A backend may be supplied with
+details of a specific request (or a group of requests in $other), but equally
+no parameters might be provided at all.
+
+Normally no parameters will be provided in the 'create' stage.  After this,
+parameters may be provided using templates.
+
+=cut
+
+sub list {
+    # -> list all ILL Requests currently placed with the backend
+    #    (we ignore all params provided)
+    my ( $self, $params ) = @_;
+    my $stage = $params->{other}->{stage};
+    if ( !$stage || $stage eq 'init' ) {
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'list',
+            stage   => 'list',
+            value   => {
+                1 => {
+                    id     => 1234,
+                    title  => "Ordering ILLs using Koha",
+                    author => "A.N. Other",
+                    status => "On order",
+                    cost   => "30 GBP",
+                },
+            },
+        };
+    } elsif ( $stage eq 'list' ) {
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'list',
+            stage   => 'commit',
+            value   => {},
+        };
+    } else {
+        # Invalid stage, return error.
+        return {
+            error   => 1,
+            status  => 'unknown_stage',
+            message => '',
+            method  => 'create',
+            stage   => $params->{stage},
+            value   => {},
+        };
+    }
+}
+
+=head3 renew
+
+  my $response = $backend->renew({
+      request    => $requestdetails,
+      other      => $other,
+  });
+
+Attempt to renew a request that was supplied through backend and is currently
+in use by us.
+
+We will generally use $request.  This will be supplied at all times through
+Illrequest.  $other may be supplied using templates.
+
+=cut
+
+sub renew {
+    # -> request a currently borrowed ILL be renewed in the backend
+    my ( $self, $params ) = @_;
+    # Turn Illrequestattributes into a plain hashref
+    my $value = {};
+    my $attributes = $params->{request}->illrequestattributes;
+    foreach my $attr (@{$attributes->as_list}) {
+        $value->{$attr->type} = $attr->value;
+    };
+    # Submit request to backend, parse response...
+    my ( $error, $status, $message ) = ( 0, '', '' );
+    if ( !$value->{status} || $value->{status} eq 'On order' ) {
+        $error = 1;
+        $status = 'not_renewed';
+        $message = 'Order not yet delivered.';
+    } else {
+        $value->{status} = "Renewed";
+    }
+    # ...then return our result:
+    return {
+        error   => $error,
+        status  => $status,
+        message => $message,
+        method  => 'renew',
+        stage   => 'commit',
+        value   => $value,
+    };
+}
+
+=head3 update_status
+
+  my $response = $backend->update_status({
+      request    => $requestdetails,
+      other      => $other,
+  });
+
+Our Illmodule is handling a request to update the status of an Illrequest.  As
+part of this we give the backend an opportunity to perform arbitrary actions
+on update to a new status.
+
+We will provide $request.  This will be supplied at all times through
+Illrequest.  $other will contain entries for the old status and the new
+status, as well as other information provided from templates.
+
+$old_status, $new_status.
+
+=cut
+
+sub update_status {
+    # -> ILL module update hook: custom actions on status update
+    my ( $self, $params ) = @_;
+    # Turn Illrequestattributes into a plain hashref
+    my $value = {};
+    my $attributes = $params->{request}->illrequestattributes;
+    foreach my $attr (@{$attributes->as_list}) {
+        $value->{$attr->type} = $attr->value;
+    };
+    # Submit request to backend, parse response...
+    my ( $error, $status, $message ) = (0, '', '');
+    my $old = $params->{other}->{old_status};
+    my $new = $params->{other}->{new_status};
+    if ( !$new || $new eq 'ERR' ) {
+        ( $error, $status, $message ) = (
+            1, 'failed_update_hook',
+            'Fake reason for failing to perform update operation.'
+        );
+    }
+    return {
+        error   => $error,
+        status  => $status,
+        message => $message,
+        method  => 'update_status',
+        stage   => 'commit',
+        value   => $value,
+    };
+}
+
+=head3 cancel
+
+  my $response = $backend->cancel({
+      request    => $requestdetails,
+      other      => $other,
+  });
+
+We will attempt to cancel a request that was confirmed.
+
+We will generally use $request.  This will be supplied at all times through
+Illrequest.  $other may be supplied using templates.
+
+=cut
+
+sub cancel {
+    # -> request an already 'confirm'ed ILL order be cancelled
+    my ( $self, $params ) = @_;
+    # Turn Illrequestattributes into a plain hashref
+    my $value = {};
+    my $attributes = $params->{request}->illrequestattributes;
+    foreach my $attr (@{$attributes->as_list}) {
+        $value->{$attr->type} = $attr->value;
+    };
+    # Submit request to backend, parse response...
+    my ( $error, $status, $message ) = (0, '', '');
+    if ( !$value->{status} ) {
+        ( $error, $status, $message ) = (
+            1, 'unknown_request', 'Cannot cancel an unknown request.'
+        );
+    } else {
+        $attributes->find({ type => "status" })->delete;
+        $params->{request}->status("REQREV");
+        $params->{request}->cost(undef);
+        $params->{request}->orderid(undef);
+        $params->{request}->store;
+    }
+    return {
+        error   => $error,
+        status  => $status,
+        message => $message,
+        method  => 'cancel',
+        stage   => 'commit',
+        value   => $value,
+    };
+}
+
+=head3 status
+
+  my $response = $backend->create({
+      request    => $requestdetails,
+      other      => $other,
+  });
+
+We will try to retrieve the status of a specific request.
+
+We will generally use $request.  This will be supplied at all times through
+Illrequest.  $other may be supplied using templates.
+
+=cut
+
+sub status {
+    # -> request the current status of a confirmed ILL order
+    my ( $self, $params ) = @_;
+    my $value = {};
+    my $stage = $params->{other}->{stage};
+    my ( $error, $status, $message ) = (0, '', '');
+    if ( !$stage || $stage eq 'init' ) {
+        # Generate status result
+        # Turn Illrequestattributes into a plain hashref
+        my $attributes = $params->{request}->illrequestattributes;
+        foreach my $attr (@{$attributes->as_list}) {
+            $value->{$attr->type} = $attr->value;
+        }
+        ;
+        # Submit request to backend, parse response...
+        if ( !$value->{status} ) {
+            ( $error, $status, $message ) = (
+                1, 'unknown_request', 'Cannot query status of an unknown request.'
+            );
+        }
+        return {
+            error   => $error,
+            status  => $status,
+            message => $message,
+            method  => 'status',
+            stage   => 'status',
+            value   => $value,
+        };
+
+    } elsif ( $stage eq 'status') {
+        # No more to do for method.  Return to illlist.
+        return {
+            error   => $error,
+            status  => $status,
+            message => $message,
+            method  => 'status',
+            stage   => 'commit',
+            next    => 'illlist',
+            value   => {},
+        };
+
+    } else {
+        # Invalid stage, return error.
+        return {
+            error   => 1,
+            status  => 'unknown_stage',
+            message => '',
+            method  => 'create',
+            stage   => $params->{stage},
+            value   => {},
+        };
+    }
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequest/Config.pm b/Koha/Illrequest/Config.pm
new file mode 100644
index 0000000..159e5e5
--- /dev/null
+++ b/Koha/Illrequest/Config.pm
@@ -0,0 +1,389 @@
+package Koha::Illrequest::Config;
+
+# Copyright 2013,2014 PTFS Europe Ltd
+#
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+use C4::Context;
+
+=head1 NAME
+
+Koha::Illrequest::Config - Koha ILL Configuration Object
+
+=head1 SYNOPSIS
+
+Object-oriented class that giving access to the illconfig data derived
+from ill/config.yaml.
+
+=head1 DESCRIPTION
+
+Config object providing abstract representation of the expected XML
+returned by ILL API.
+
+In particular the config object uses a YAML file, whose path is
+defined by <illconfig> in koha-conf.xml. That YAML file provides the
+data structure exposed in this object.
+
+By default the configured data structure complies with fields used by
+the British Library Interlibrary Loan DSS API.
+
+The config file also provides mappings for Record Object accessors.
+
+FIXME: illcomm: In general Config should be split into two parts:
+those that are high-level and those that are backend specific.  The
+latter should not get specific accessors, but rather a generic
+accessor, that takes a setting name as string parameter.
+
+=head1 API
+
+=head2 Class Methods
+
+=head3 new
+
+    my $config = Koha::Illrequest::Config->new();
+
+Create a new Koha::Illrequest::Config object, with mapping data loaded from the
+ILL configuration file.
+
+=cut
+
+sub new {
+    my ( $class, $test ) = @_;
+    my $self  = {};
+
+    $self->{configuration} = _load_configuration(
+        C4::Context->config("interlibrary_loans"),
+        C4::Context->preference("UnmediatedILL")
+      ) unless ( $test );
+
+    bless $self, $class;
+
+    return $self;
+}
+
+=head3 backend
+
+    $backend = $config->backend($name);
+    $backend = $config->backend;
+
+Standard setter/accessor for our backend.
+
+=cut
+
+sub backend {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{backend} = $new if $new;
+    return $self->{configuration}->{backend};
+}
+
+=head3 backend_dir
+
+    $backend_dir = $config->backend_dir($new_path);
+    $backend_dir = $config->backend_dir;
+
+Standard setter/accessor for our backend_directory.
+
+=cut
+
+sub backend_dir {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{backend_directory} = $new if $new;
+    return $self->{configuration}->{backend_directory};
+}
+
+=head3 partner_code
+
+    $partner_code = $config->partner_code($new_path);
+    $partner_code = $config->partner_code;
+
+Standard setter/accessor for our partner_code.
+
+=cut
+
+sub partner_code {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{partner_code} = $new if $new;
+    return $self->{configuration}->{partner_code};
+}
+
+=head3 limits
+
+    $limits = $config->limits($limitshash);
+    $limits = $config->limits;
+
+Standard setter/accessor for our limits.  No parsing is performed on
+$LIMITSHASH, so caution should be exercised when using this setter.
+
+=cut
+
+sub limits {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{limits} = $new if $new;
+    return $self->{configuration}->{limits};
+}
+
+=head3 getPrefixes
+
+    my $prefixes = $config->getPrefixes('brw_cat' | 'branch');
+
+Return the prefix for ILLs defined by our config.
+
+=cut
+
+sub getPrefixes {
+    my ( $self, $type ) = @_;
+    die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
+    my $values = $self->{configuration}->{prefixes}->{$type};
+    $values->{default} = $self->{configuration}->{prefixes}->{default};
+    return $values;
+}
+
+=head3 getLimitRules
+
+    my $rules = $config->getLimitRules('brw_cat' | 'branch')
+
+Return the hash of ILL limit rules defined by our config.
+
+=cut
+
+sub getLimitRules {
+    my ( $self, $type ) = @_;
+    die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
+    my $values = $self->{configuration}->{limits}->{$type};
+    $values->{default} = $self->{configuration}->{limits}->{default};
+    return $values;
+}
+
+=head3 getDigitalRecipients
+
+    my $recipient_rules= $config->getDigitalRecipients('brw_cat' | 'branch');
+
+Return the hash of digital_recipient settings defined by our config.
+
+=cut
+
+sub getDigitalRecipients {
+    my ( $self, $type ) = @_;
+    die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
+    my $values = $self->{configuration}->{digital_recipients}->{$type};
+    $values->{default} =
+        $self->{configuration}->{digital_recipients}->{default};
+    return $values;
+}
+
+=head3 censorship
+
+    my $censoredValues = $config->censorship($hash);
+    my $censoredValues = $config->censorship;
+
+Standard setter/accessor for our limits.  No parsing is performed on $HASH, so
+caution should be exercised when using this setter.
+
+Return our censorship values for the OPAC as loaded from the koha-conf.xml, or
+the fallback value (no censorship).
+
+=cut
+
+sub censorship {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{censorship} = $new if $new;
+    return $self->{configuration}->{censorship};
+}
+
+=head3 _load_configuration
+
+    my $configuration = $config->_load_configuration($config_from_xml);
+
+Read the configuration values passed as the parameter, and populate a hashref
+suitable for use with these.
+
+A key task performed here is the parsing of the input in the configuration
+file to ensure we have only valid input there.
+
+=cut
+
+sub _load_configuration {
+    my ( $xml_config, $unmediated ) = @_;
+    my $xml_backend_dir = $xml_config->{backend_directory};
+
+    # Default data structure to be returned
+    my $configuration = {
+        backend_directory  => $xml_backend_dir,
+        censorship         => {
+            censor_notes_staff => 0,
+            censor_reply_date => 0,
+        },
+        limits             => {},
+        digital_recipients => {},
+        prefixes           => {},
+        partner_code       => 'ILLLIBS',
+    };
+
+    # Per Branch Configuration
+    my $branches = $xml_config->{branch};
+    if ( ref($branches) eq "ARRAY" ) {
+        # Multiple branch overrides defined
+        map {
+            _load_unit_config({
+                unit   => $_,
+                id     => $_->{code},
+                config => $configuration,
+                type   => 'branch'
+            })
+        } @{$branches};
+    } elsif ( ref($branches) eq "HASH" ) {
+        # Single branch override defined
+        _load_unit_config({
+            unit   => $branches,
+            id     => $branches->{code},
+            config => $configuration,
+            type   => 'branch'
+        });
+    }
+
+    # Per Borrower Category Configuration
+    my $brw_cats = $xml_config->{borrower_category};
+    if ( ref($brw_cats) eq "ARRAY" ) {
+        # Multiple borrower category overrides defined
+        map {
+            _load_unit_config({
+                unit   => $_,
+                id     => $_->{code},
+                config => $configuration,
+                type   => 'brw_cat'
+            })
+        } @{$brw_cats};
+    } elsif ( ref($brw_cats) eq "HASH" ) {
+        # Single branch override defined
+        _load_unit_config({
+            unit   => $brw_cats,
+            id     => $brw_cats->{code},
+            config => $configuration,
+            type   => 'brw_cat'
+        });
+    }
+
+    # Default Configuration
+    _load_unit_config({
+        unit   => $xml_config,
+        id     => 'default',
+        config => $configuration
+    });
+
+    # Censorship
+    my $staff_comments = $xml_config->{staff_request_comments};
+    $configuration->{censorship}->{censor_notes_staff} = 1
+        if ( $staff_comments && 'hide' eq $staff_comments );
+    my $reply_date = $xml_config->{reply_date};
+    if ( 'hide' eq $reply_date ) {
+        $configuration->{censorship}->{censor_reply_date} = 1;
+    } else {
+        $configuration->{censorship}->{censor_reply_date} = $reply_date;
+    }
+
+    # ILL Partners
+    $configuration->{partner_code} = $xml_config->{partner_code} || 'ILLLIBS';
+
+    die "No DEFAULT_FORMATS has been defined in koha-conf.xml, but UNMEDIATEDILL is active."
+        if ( $unmediated && !$configuration->{default_formats}->{default} );
+
+    return $configuration;
+}
+
+=head3 _load_unit_config
+
+    my $configuration->{part} = _load_unit_config($params);
+
+$PARAMS is a hashref with the following elements:
+- unit: the part of the configuration we are parsing.
+- id: the name within which we will store the parsed unit in config.
+- config: the configuration we are augmenting.
+- type: the type config unit we are parsing.  Assumed to be 'default'.
+
+Read `unit', and augment `config' with these under `id'.
+
+This is a helper for _load_configuration.
+
+A key task performed here is the parsing of the input in the configuration
+file to ensure we have only valid input there.
+
+=cut
+
+sub _load_unit_config {
+    my ( $params ) = @_;
+    my $unit = $params->{unit};
+    my $id = $params->{id};
+    my $config = $params->{config};
+    my $type = $params->{type};
+    return $config unless $id;
+
+    if ( $unit->{api_key} && $unit->{api_auth} ) {
+        $config->{credentials}->{api_keys}->{$id} = {
+            api_key  => $unit->{api_key},
+            api_auth => $unit->{api_auth},
+        };
+    }
+    # Add request_limit rules.
+    # METHOD := 'annual' || 'active'
+    # COUNT  := x >= -1
+    if ( ref $unit->{request_limit} eq 'HASH' ) {
+        my $method  = $unit->{request_limit}->{method};
+        my $count = $unit->{request_limit}->{count};
+        if ( 'default' eq $id ) {
+            $config->{limits}->{$id}->{method}  = $method
+                if ( $method && ( 'annual' eq $method || 'active' eq $method ) );
+            $config->{limits}->{$id}->{count} = $count
+                if ( $count && ( -1 <= $count ) );
+        } else {
+            $config->{limits}->{$type}->{$id}->{method}  = $method
+                if ( $method && ( 'annual' eq $method || 'active' eq $method ) );
+            $config->{limits}->{$type}->{$id}->{count} = $count
+                if ( $count && ( -1 <= $count ) );
+        }
+    }
+
+    # Add prefix rules.
+    # PREFIX := string
+    if ( $unit->{prefix} ) {
+        if ( 'default' eq $id ) {
+            $config->{prefixes}->{$id} = $unit->{prefix};
+        } else {
+            $config->{prefixes}->{$type}->{$id} = $unit->{prefix};
+        }
+    }
+
+    # Add digital_recipient rules.
+    # DIGITAL_RECIPIENT := borrower || branch (defaults to borrower)
+    if ( $unit->{digital_recipient} ) {
+        if ( 'default' eq $id ) {
+            $config->{digital_recipients}->{$id} = $unit->{digital_recipient};
+        } else {
+            $config->{digital_recipients}->{$type}->{$id} =
+                $unit->{digital_recipient};
+        }
+    }
+
+    return $config;
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequestattribute.pm b/Koha/Illrequestattribute.pm
new file mode 100644
index 0000000..16bc086
--- /dev/null
+++ b/Koha/Illrequestattribute.pm
@@ -0,0 +1,51 @@
+package Koha::Illrequestattribute;
+
+# Copyright PTFS Europe 2016
+#
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+
+use Koha::Database;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::Illrequestattribute - Koha Illrequestattribute Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequestattribute';
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequestattributes.pm b/Koha/Illrequestattributes.pm
new file mode 100644
index 0000000..e05fa6e
--- /dev/null
+++ b/Koha/Illrequestattributes.pm
@@ -0,0 +1,55 @@
+package Koha::Illrequestattributes;
+
+# Copyright PTFS Europe 2016
+#
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+
+use Koha::Database;
+use Koha::Illrequestattribute;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::Illrequestattributes - Koha Illrequestattributes Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequestattribute';
+}
+
+sub object_class {
+    return 'Koha::Illrequestattribute';
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequests.pm b/Koha/Illrequests.pm
new file mode 100644
index 0000000..4bfd865
--- /dev/null
+++ b/Koha/Illrequests.pm
@@ -0,0 +1,99 @@
+package Koha::Illrequests;
+
+# Copyright PTFS Europe 2016
+#
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+
+use Koha::Database;
+use Koha::Illrequest;
+use Koha::Illrequest::Config;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::Illrequests - Koha Illrequests Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequest';
+}
+
+sub object_class {
+    return 'Koha::Illrequest';
+}
+
+##### To be implemented Facade
+
+=head3 new
+
+    my $illRequests = Koha::Illrequests->new();
+
+Create an ILLREQUESTS object, a singleton through which we can interact with
+ILLREQUEST objects stored in the database or search for ILL candidates at API
+backends.
+
+=cut
+
+sub new {
+    my ( $class, $attributes ) = @_;
+
+    my $self = $class->SUPER::new($class, $attributes);
+
+    # FIXME: Unless we can inject config into Illrequest objects, strip this
+    # out.
+    my $config = Koha::Illrequest::Config->new; # <- Necessary
+    $self->{_config} = $config;                 # <- Necessary
+
+    return $self;
+}
+
+=head3 search_incomplete
+
+    my $requests = $illRequests->search_incomplete;
+
+A specialised version of `search`, returning all requests currently
+not considered completed.
+
+=cut
+
+sub search_incomplete {
+    my ( $self ) = @_;
+    $self->search( {
+        status => [
+            -and => { '!=', 'COMP' }, { '!=', 'GENCOMP' }
+        ]
+    } );
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/REST/V1/Illrequests.pm b/Koha/REST/V1/Illrequests.pm
new file mode 100644
index 0000000..0807eb1
--- /dev/null
+++ b/Koha/REST/V1/Illrequests.pm
@@ -0,0 +1,85 @@
+package Koha::REST::V1::Illrequests;
+
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+
+use Koha::Illrequests;
+use Koha::Library;
+
+sub list {
+    my ($c, $args, $cb) = @_;
+
+    my $filter;
+    $args //= {};
+    my $output = [];
+
+    # Create a hash where all keys are embedded values
+    # Enables easy checking
+    my %embed;
+    if (defined $args->{embed}) {
+        %embed = map { $_ => 1 }  @{$args->{embed}};
+        delete $args->{embed};
+    }
+
+    for my $filter_param ( keys %$args ) {
+        my @values = split(/,/, $args->{$filter_param});
+        $filter->{$filter_param} = \@values;
+    }
+
+    my $requests = Koha::Illrequests->search($filter);
+
+    while (my $request = $requests->next) {
+        my $unblessed = $request->unblessed;
+        # Add the request's id_prefix
+        $unblessed->{id_prefix} = $request->id_prefix;
+        # Augment the request response with patron details
+        # if appropriate
+        if (defined $embed{patron}) {
+            my $patron = $request->patron;
+            $unblessed->{patron} = {
+                firstname  => $patron->firstname,
+                surname    => $patron->surname,
+                cardnumber => $patron->cardnumber
+            };
+        }
+        # Augment the request response with metadata details
+        # if appropriate
+        if (defined $embed{metadata}) {
+            $unblessed->{metadata} = $request->metadata;
+        }
+        # Augment the request response with status details
+        # if appropriate
+        if (defined $embed{capabilities}) {
+            $unblessed->{capabilities} = $request->capabilities;
+        }
+        # Augment the request response with branch details
+        # if appropriate
+        if (defined $embed{branch}) {
+            $unblessed->{branch} = Koha::Libraries->find(
+                $request->branchcode
+            )->unblessed;
+        }
+        push @{$output}, $unblessed
+    }
+
+    return $c->$cb( $output, 200 );
+
+}
+
+1;
diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json
index b1fcf40..608d717 100644
--- a/api/v1/swagger/paths.json
+++ b/api/v1/swagger/paths.json
@@ -16,5 +16,8 @@
   },
   "/patrons/{borrowernumber}": {
     "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}"
+  },
+  "/illrequests": {
+    "$ref": "paths/illrequests.json#/~1illrequests"
   }
 }
diff --git a/api/v1/swagger/paths/illrequests.json b/api/v1/swagger/paths/illrequests.json
new file mode 100644
index 0000000..ddafd80
--- /dev/null
+++ b/api/v1/swagger/paths/illrequests.json
@@ -0,0 +1,98 @@
+{
+    "/illrequests": {
+        "get": {
+            "x-mojo-controller": "Koha::REST::V1::Illrequests",
+            "operationId": "list",
+            "tags": ["illrequests"],
+            "parameters": [{
+                "name": "embed",
+                "in": "query",
+                "description": "Additional objects that should be embedded in the response",
+                "required": false,
+                "type": "array",
+                "collectionFormat": "csv",
+                "items": {
+                    "type": "string",
+                    "enum": [
+                        "patron",
+                        "branch",
+                        "capabilities"
+                    ]
+                }
+            }, {
+                "name": "backend",
+                "in": "query",
+                "description": "The name of a ILL backend",
+                "required": false,
+                "type": "string"
+            }, {
+                "name": "orderid",
+                "in": "query",
+                "description": "The order ID of a request",
+                "required": false,
+                "type": "string"
+            }, {
+                "name": "biblio_id",
+                "in": "query",
+                "description": "The biblio ID associated with a request",
+                "required": false,
+                "type": "integer"
+            }, {
+                "name": "borrower_id",
+                "in": "query",
+                "description": "The borrower ID associated with a request",
+                "required": false,
+                "type": "integer"
+            }, {
+                "name": "completed",
+                "in": "query",
+                "description": "The date the request was considered completed",
+                "required": false,
+                "type": "string"
+            }, {
+                "name": "status",
+                "in": "query",
+                "description": "A full status string e.g. REQREV",
+                "required": false,
+                "type": "string"
+            }, {
+                "name": "medium",
+                "in": "query",
+                "description": "The medium of the requested item",
+                "required": false,
+                "type": "string"
+            }, {
+                "name": "updated",
+                "in": "query",
+                "description": "The last updated date of the request",
+                "required": false,
+                "type": "string"
+            }, {
+                "name": "placed",
+                "in": "query",
+                "description": "The date the request was placed",
+                "required": false,
+                "type": "string"
+            }, {
+                "name": "branch_id",
+                "in": "query",
+                "description": "The ID of the pickup branch",
+                "required": false,
+                "type": "string"
+            }],
+            "produces": [
+                "application/json"
+            ],
+            "responses": {
+                "200": {
+                    "description": "OK"
+                }
+            },
+            "x-koha-authorization": {
+                "permissions": {
+                    "borrowers": "1"
+                }
+            }
+        }
+    }
+}
diff --git a/etc/koha-conf.xml b/etc/koha-conf.xml
index 4ccee3a..067e978 100644
--- a/etc/koha-conf.xml
+++ b/etc/koha-conf.xml
@@ -153,5 +153,26 @@ __PAZPAR2_TOGGLE_XML_POST__
  <plack_max_requests>50</plack_max_requests>
  <plack_workers>2</plack_workers>
 
+ <interlibrary_loans>
+     <!-- Path to where Illbackends are located on the system
+          - This setting should normally not be touched -->
+     <backend_directory>__PERL_MODULE_DIR__/Koha/Illbackends</backend_directory>
+     <!-- How should we treat staff comments?
+          - hide: don't show in OPAC
+          - show: show in OPAC -->
+     <staff_request_comments>hide</staff_request_comments>
+     <!-- How should we treat the reply_date field?
+          - hide: don't show this field in the UI
+          - any other string: show, with this label -->
+     <reply_date>hide</reply_date>
+     <!-- Where should digital ILLs be sent?
+          - borrower: send it straight to the borrower email
+          - branch: send the ILL to the branch email -->
+     <digital_recipient>branch</digital_recipient>
+     <!-- What patron category should we use for p2p ILL requests?
+          - By default this is set to 'ILLLIBS' -->
+     <partner_code>ILLLIBS</partner_code>
+ </interlibrary_loans>
+
 </config>
 </yazgfs>
diff --git a/ill/ill-requests.pl b/ill/ill-requests.pl
new file mode 100755
index 0000000..a267a8d
--- /dev/null
+++ b/ill/ill-requests.pl
@@ -0,0 +1,238 @@
+#!/usr/bin/perl
+
+# Copyright 2013 PTFS-Europe Ltd and Mark Gavillet
+# Copyright 2014 PTFS-Europe Ltd
+#
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+
+use CGI;
+
+use C4::Auth;
+use C4::Output;
+use Koha::AuthorisedValues;
+use Koha::Illrequests;
+use Koha::Libraries;
+
+my $cgi = CGI->new;
+my $illRequests = Koha::Illrequests->new;
+
+# Grab all passed data
+# 'our' since Plack changes the scoping
+# of 'my'
+our $params = $cgi->Vars();
+
+my $op = $params->{method} || 'illlist';
+
+my ( $template, $patronnumber, $cookie ) = get_template_and_user( {
+    template_name => 'ill/ill-requests.tt',
+    query         => $cgi,
+    type          => 'intranet',
+    flagsrequired => { ill => '*' },
+} );
+
+if ( $op eq 'illview' ) {
+    # View the details of an ILL
+    my $request = Koha::Illrequests->find($params->{illrequest_id});
+
+    $template->param(
+        request => $request
+    );
+
+} elsif ( $op eq 'create' ) {
+    # We're in the process of creating a request
+    my $request = Koha::Illrequest->new
+        ->load_backend($params->{backend});
+    my $backend_result = $request->backend_create($params);
+    $template->param(
+        whole   => $backend_result,
+        request => $request
+    );
+    handle_commit_maybe($backend_result, $request);
+
+} elsif ( $op eq 'confirm' ) {
+    # Backend 'confirm' method
+    # confirm requires a specific request, so first, find it.
+    my $request = Koha::Illrequests->find($params->{illrequest_id});
+    my $backend_result = $request->backend_confirm($params);
+    $template->param(
+        whole   => $backend_result,
+        request => $request,
+    );
+
+    # handle special commit rules & update type
+    handle_commit_maybe($backend_result, $request);
+
+} elsif ( $op eq 'cancel' ) {
+    # Backend 'cancel' method
+    # cancel requires a specific request, so first, find it.
+    my $request = Koha::Illrequests->find($params->{illrequest_id});
+    my $backend_result = $request->backend_cancel($params);
+    $template->param(
+        whole   => $backend_result,
+        request => $request,
+    );
+
+    # handle special commit rules & update type
+    handle_commit_maybe($backend_result, $request);
+
+} elsif ( $op eq 'edit_action' ) {
+    # Handle edits to the Illrequest object.
+    # (not the Illrequestattributes)
+    # We simulate the API for backend requests for uniformity.
+    # So, init:
+    my $request = Koha::Illrequests->find($params->{illrequest_id});
+    if ( !$params->{stage} ) {
+        my $backend_result = {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'edit_action',
+            stage   => 'init',
+            next    => '',
+            value   => {}
+        };
+        $template->param(
+            whole   => $backend_result,
+            request => $request
+        );
+    } else {
+        # Commit:
+        # Save the changes
+        $request->borrowernumber($params->{borrowernumber});
+        $request->biblio_id($params->{biblio_id});
+        $request->branchcode($params->{branchcode});
+        $request->notesopac($params->{notesopac});
+        $request->notesstaff($params->{notesstaff});
+        $request->store;
+        my $backend_result = {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'edit_action',
+            stage   => 'commit',
+            next    => 'illlist',
+            value   => {}
+        };
+        handle_commit_maybe($backend_result, $request);
+    }
+
+} elsif ( $op eq 'moderate_action' ) {
+    # Moderate action is required for an ILL submodule / syspref.
+    # Currently still needs to be implemented.
+    redirect_to_list();
+
+} elsif ( $op eq 'delete_confirm') {
+    my $request = Koha::Illrequests->find($params->{illrequest_id});
+
+    $template->param(
+        request => $request
+    );
+
+} elsif ( $op eq 'delete' ) {
+
+    # Check if the request is confirmed, if not, redirect
+    # to the confirmation view
+    if ($params->{confirmed} == 1) {
+        # We simply delete the request...
+        my $request = Koha::Illrequests->find(
+            $params->{illrequest_id}
+        )->delete;
+        # ... then return to list view.
+        redirect_to_list();
+    } else {
+        print $cgi->redirect(
+            "/cgi-bin/koha/ill/ill-requests.pl?" .
+            "method=delete_confirm&illrequest_id=" .
+            $params->{illrequest_id});
+    }
+
+} elsif ( $op eq 'generic_confirm' ) {
+    my $request = Koha::Illrequests->find($params->{illrequest_id});
+    $params->{current_branchcode} = C4::Context->mybranch;
+    my $backend_result = $request->generic_confirm($params);
+    $template->param(
+        whole => $backend_result,
+        request => $request,
+    );
+
+    # handle special commit rules & update type
+    handle_commit_maybe($backend_result, $request);
+
+} elsif ( $op eq 'illlist') {
+    # Display all current ILLs
+    my $requests = $illRequests->search();
+
+    $template->param(
+        requests => $requests
+    );
+
+    # If we receive a pre-filter, make it available to the template
+    my $possible_filters = ['borrowernumber'];
+    my $active_filters = [];
+    foreach my $filter(@{$possible_filters}) {
+        if ($params->{$filter}) {
+            push @{$active_filters},
+                { name => $filter, value => $params->{$filter}};
+        }
+    }
+    if (scalar @{$active_filters} > 0) {
+        $template->param(
+            prefilters => $active_filters
+        );
+    }
+} else {
+    my $request = Koha::Illrequests->find($params->{illrequest_id});
+    my $backend_result = $request->custom_capability($op, $params);
+    $template->param(
+        whole => $backend_result,
+        request => $request,
+    );
+}
+
+# Get a list of backends
+my $ir = Koha::Illrequest->new;
+
+$template->param(
+    backends    => $ir->available_backends,
+    media       => [ "Book", "Article", "Journal" ],
+    query_type  => $op,
+    branches    => Koha::Libraries->search->unblessed,
+    here_link   => "/cgi-bin/koha/ill/ill-requests.pl"
+);
+
+output_html_with_http_headers( $cgi, $cookie, $template->output );
+
+sub handle_commit_maybe {
+    my ( $backend_result, $request ) = @_;
+    # We need to special case 'commit'
+    if ( $backend_result->{stage} eq 'commit' ) {
+        if ( $backend_result->{next} eq 'illview' ) {
+            # Redirect to a view of the newly created request
+            print $cgi->redirect(
+                '/cgi-bin/koha/ill/ill-requests.pl?method=illview&illrequest_id='.
+                $request->id
+            );
+        } else {
+            # Redirect to a requests list view
+            redirect_to_list();
+        }
+    }
+}
+
+sub redirect_to_list {
+    print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl');
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css
index 784fc3e..e86043d 100644
--- a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css
+++ b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css
@@ -3019,3 +3019,80 @@ fieldset.rows + fieldset.action {
 #patron_search #filters {
     display: none;
 }
+
+#interlibraryloans h1 {
+    margin: 1em 0;
+}
+
+#interlibraryloans h2 {
+    margin-bottom: 20px;
+}
+
+#interlibraryloans h3 {
+    margin-top: 20px;
+}
+
+#interlibraryloans .bg-info {
+    overflow: auto;
+    position: relative;
+}
+
+#interlibraryloans #search-summary {
+    -webkit-transform: translateY(-50%);
+    -ms-transform: translateY(-50%);
+    -o-transform: translateY(-50%);
+    transform: translateY(-50%);
+    position: absolute;
+    top: 50%;
+}
+
+#interlibraryloans .format h5 {
+    margin-top: 20px;
+}
+
+#interlibraryloans .format li {
+    list-style: none;
+}
+
+#interlibraryloans .format h4 {
+    margin-bottom: 20px;
+}
+
+#interlibraryloans .format input {
+    margin: 10px 0;
+}
+
+#interlibraryloans #freeform-fields .custom-name {
+    width: 9em;
+    margin-right: 1em;
+    text-align: right;
+}
+
+#interlibraryloans #freeform-fields .delete-new-field {
+    margin-left: 1em;
+}
+
+#interlibraryloans #add-new-fields {
+    margin: 1em;
+}
+
+#ill-view-panel {
+    margin-top: 15px;
+}
+
+#ill-view-panel h3 {
+    margin-bottom: 10px;
+}
+
+#ill-view-panel h4 {
+    margin-bottom: 20px;
+}
+
+#ill-view-panel .rows div {
+    height: 1em;
+    margin-bottom: 1em;
+}
+
+#ill-view-panel #requestattributes .label {
+    width: auto;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc
index e621d64..4006f6c 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc
@@ -112,5 +112,8 @@
     [% IF Koha.Preference('HouseboundModule') %]
         [% IF houseboundview %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/members/housebound.pl?borrowernumber=[% borrowernumber %]">Housebound</a></li>
     [% END %]
+    [% IF Koha.Preference('ILLModule') %]
+        <li><a href="/cgi-bin/koha/ill/ill-requests.pl?borrowernumber=[% borrowernumber %]">Interlibrary loans</a></li>
+    [% END %]
 </ul></div>
 [% END %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc
new file mode 100644
index 0000000..3f541d9
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc
@@ -0,0 +1,24 @@
+[% USE Koha %]
+[% IF Koha.Preference('ILLModule ') %]
+    <div id="toolbar" class="btn-toolbar">
+        [% IF backends.size > 1 %]
+            <div class="dropdown btn-group">
+                <button class="btn btn-sm btn-default dropdown-toggle" type="button" id="ill-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                    <i class="fa fa-plus"></i> New ILL request <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu" aria-labelledby="ill-backend-dropdown">
+                    [% FOREACH backend IN backends %]
+                        <li><a href="/cgi-bin/koha/ill/ill-requests.pl?method=create&amp;backend=[% backend %]">[% backend %]</a></li>
+                    [% END %]
+                </ul>
+            </div>
+        [% ELSE %]
+            <a id="ill-new" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=create&amp;backend=[% backends.0 %]">
+                <i class="fa fa-plus"></i> New ILL request
+            </a>
+        [% END %]
+        <a id="ill-list" class="btn btn-sm btn-default btn-group" href="/cgi-bin/koha/ill/ill-requests.pl">
+            <i class="fa fa-list"></i> List requests
+        </a>
+    </div>
+[% END %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc
index 7c6275a..4a1ce81 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc
@@ -20,6 +20,7 @@
     [%- CASE 'plugins' -%]<span>Koha plugins</span>
     [%- CASE 'lists' -%]<span>Lists</span>
     [%- CASE 'clubs' -%]<span>Patron clubs</span>
+    [%- CASE 'ill' -%]<span>Create and modify Interlibrary loan requests</span>
     [%- END -%]
 [%- END -%]
 
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt
new file mode 100644
index 0000000..201a977
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt
@@ -0,0 +1,487 @@
+[% USE Branches %]
+
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; ILL requests  &rsaquo;</title>
+[% INCLUDE 'doc-head-close.inc' %]
+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.tablesorter.min.js"></script>
+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
+<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/datatables.css">
+[% INCLUDE 'datatables.inc' %]
+<script type="text/javascript">
+    //<![CDATA[
+    $(document).ready(function() {
+        var myTable = $("#ill-requests").DataTable($.extend(true, {}, dataTablesDefaults, {
+            "aoColumnDefs": [  // Last column shouldn't be sortable or searchable
+                { "aTargets": [ -1 ], "bSortable": false, "bSearchable": false },
+            ],
+            "aaSorting": [[ 8, "desc" ]], // Default sort, updated descending
+            "processing": true, // Display a message when manipulating
+            "language": {
+                "loadingRecords": "Please wait - loading requests...",
+                "zeroRecords": "No requests were found"
+            },
+            "iDisplayLength": 10, // 10 results per page
+            "sPaginationType": "full_numbers", // Pagination display
+            "sAjaxDataProp": "", // Data is in the root object of the response
+            "deferRender": true, // Improve performance on big datasets
+            "ajax": {
+                url: "/api/v1/illrequests?embed=metadata,patron,capabilities,branch",
+                cache: true
+            },
+            "columns": [
+                {
+                    data: 'orderid',
+                    className: 'orderid'
+                },
+                {
+                    render: function(data, type, row) {
+                        return (
+                            row.metadata.hasOwnProperty('Author') &&
+                            row.metadata.Author.length > 0
+                        ) ? row.metadata.Author : 'N/A';
+                    },
+                    className: 'title'
+                },
+                {
+                    render: function(data, type, row) {
+                        return (
+                            row.metadata.hasOwnProperty('Title') &&
+                            row.metadata.Title.length > 0
+                        ) ? row.metadata.Title : 'N/A';
+                    },
+                    className: 'title'
+                },
+                {
+                    render: function(data, type, row) {
+                        return '<a title="View borrower details" ' +
+                        'href="/cgi-bin/koha/members/moremember.pl?' +
+                        'borrowernumber='+row.borrowernumber+'">' +
+                        row.patron.firstname + ' ' + row.patron.surname +
+                        '</a>';
+                    },
+                    className: 'borrower_name'
+                },
+                {
+                    data: 'borrowernumber',
+                    className: 'borrowernumber'
+                },
+                {
+                    data: 'biblio_id',
+                    className: 'biblio_id'
+                },
+                {
+                    data: 'branch.branchname',
+                    className: 'branch_name'
+                },
+                {
+                    render: function(data, type, row) {
+                        return row.capabilities[row.status].name;
+                    },
+                    className: 'status'
+                },
+                {
+                    data: 'updated',
+                    className: 'updated'
+                },
+                {
+                    data: 'medium',
+                    className: 'medium'
+                },
+                {
+                    data: 'cost',
+                    className: 'cost'
+                },
+                {
+                    render: function(data, type, row) {
+                        return row.id_prefix + row.illrequest_id;
+                    },
+                    className: 'request_id'
+                },
+                {
+                    data: null,
+                    render: function(data, type, row) {
+                        return '<a class="btn btn-default btn-sm" ' +
+                        'href="/cgi-bin/koha/ill/ill-requests.pl?' +
+                        'method=illview&amp;illrequest_id=' +
+                        row.illrequest_id +
+                        '">Manage request</a>' +
+                        '</div>'
+                    }
+                }
+            ]
+        }));
+        var filters = $('#ill-requests').data();
+        if (typeof filters !== 'undefined') {
+            var filterNames = Object.keys(filters).filter(
+                function(thisData) {
+                    return thisData.match(/^filter/);
+                }
+            );
+            filterNames.forEach(function(thisFilter) {
+                var filterClass = "." + toColumnName(thisFilter);
+                var regex = '^'+filters[thisFilter]+'$';
+                myTable.columns(filterClass).search(regex, true, false);
+            });
+            myTable.draw();
+        }
+
+        function toColumnName(myVal) {
+            return myVal
+                .replace(/^filter/, '')
+                .replace(/([A-Z])/g, "_$1")
+                .replace(/^_/,'').toLowerCase();
+        };
+
+        $('#toggle_requestattributes').click(function() {
+            $('#requestattributes').toggleClass('content_hidden');
+        });
+
+        $('#partner_filter').keyup(function() {
+            var needle = $('#partner_filter').val();
+            $('#partners > option').each(function() {
+                var regex = new RegExp(needle, 'i');
+                if (
+                    needle.length == 0 ||
+                    $(this).is(':selected') ||
+                    $(this).text().match(regex)
+                ) {
+                    $(this).show();
+                } else {
+                    $(this).hide();
+                }
+            });
+        });
+
+    });
+    //]]>
+</script>
+</head>
+
+<body id="acq_suggestion" class="acq">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
+    [% IF query_type == 'create' %]
+        <a href=[% parent %]>ILL requests</a> &rsaquo; New request
+    [% ELSIF query_type == 'status' %]
+        <a href=[% parent %]>ILL requests</a> &rsaquo; Status
+    [% ELSE %]
+        ILL requests
+    [% END %]
+</div>
+
+<div id="doc3" class="yui-t2">
+    <div id="bd">
+        <div id="yui-main">
+            <div id="interlibraryloans" class="yui-b">
+                [% INCLUDE 'ill-toolbar.inc' %]
+
+                [% IF whole.error %]
+                    <h1>Error performing operation</h1>
+                    <!-- Dispatch on Status -->
+                    <p>We encountered an error:</p>
+                    <ol>
+                        <li>[% whole.status %]</li>
+                        <li>[% whole.message %]</li>
+                    </ol>
+                [% END %]
+
+                [% IF query_type == 'create' %]
+                    <h1>New ILL request</h1>
+                    [% PROCESS $whole.template %]
+
+                [% ELSIF query_type == 'confirm' %]
+                    <h1>Confirm ILL request</h1>
+                    [% PROCESS $whole.template %]
+
+                [% ELSIF query_type == 'cancel' %]
+                    <h1>Cancel an confirmed request</h1>
+                    [% PROCESS $whole.template %]
+
+                [% ELSIF query_type == 'generic_confirm' %]
+                    <h1>Place request with partner libraries</h1>
+                    <!-- Start of GENERIC_EMAIL case -->
+                    [% IF whole.value.partners %]
+                       [% ill_url = here_link _ "?method=illview&illrequest_id=" _ request.illrequest_id %]
+                        <form method="POST" action=[% here_link %]>
+                            <fieldset class="rows">
+                                <legend>Interlibrary loan request details</legend>
+                                <ol>
+                                    <li>
+                                        <label for="partner_filter">Filter partner libraries:</label>
+                                        <input type="text" id="partner_filter">
+                                    </li>
+                                    <li>
+                                        <label for="partners">Select partner libraries:</label>
+                                        <select size="5" multiple="true" id="partners"
+                                                name="partners">
+                                            [% FOREACH partner IN whole.value.partners %]
+                                                <option value=[% partner.email %]>
+                                                    [% partner.branchcode _ " - " _ partner.surname %]
+                                                </option>
+                                            [% END %]
+                                        </select>
+
+                                    </li>
+                                    <li>
+                                        <label for="subject">Subject Line</label>
+                                        <input type="text" name="subject"
+                                               id="subject" type="text"
+                                               value="[% whole.value.draft.subject %]"/>
+                                    </li>
+                                    <li>
+                                        <label for="body">Email text:</label>
+                                        <textarea name="body" id="body" rows="20" cols="80">[% whole.value.draft.body %]</textarea>
+                                    </li>
+                                </ol>
+                                <input type="hidden" value="generic_confirm" name="method">
+                                <input type="hidden" value="draft" name="stage">
+                                <input type="hidden" value="[% request.illrequest_id %]" name="illrequest_id">
+                            </fieldset>
+                            <fieldset class="action">
+                                <input type="submit" class="btn btn-default" value="Send email"/>
+                                <span><a href="[% ill_url %]" title="Return to request details">Cancel</a></span>
+                            </fieldset>
+                        </form>
+                    [% ELSE %]
+                        <fieldset class="rows">
+                            <legend>Interlibrary loan request details</legend>
+                            <p>No partners have been defined yet. Please create appropriate patron records (by default ILLLIBS category).</p>
+                            <p>Be sure to provide email addresses for these patrons.</p>
+                            <p><span><a href="[% ill_url %]" title="Return to request details">Cancel</a></span></p>
+                        </fieldset>
+                    [% END %]
+                <!-- generic_confirm ends here -->
+
+                [% ELSIF query_type == 'edit_action' %]
+                    <form method="POST" action=[% here_link %]>
+                        <fieldset class="rows">
+                            <legend>Request details</legend>
+                            <ol>
+                                <li class="borrowernumber">
+                                    <label for="borrowernumber">Borrower number:</label>
+                                    <input name="borrowernumber" id="borrowernumber" type="text" value="[% request.borrowernumber %]">
+                                </li>
+                                <li class="biblio_id">
+                                    <label for="biblio_id" class="biblio_id">Biblio number:</label>
+                                    <input name="biblio_id" id="biblio_id" type="text" value="[% request.biblio_id %]">
+                                </li>
+                                <li class="branchcode">
+                                    <label for="branchcode" class="branchcode">Branch:</label>
+                                    <select name="branchcode" id="branch">
+                                        [% FOREACH branch IN branches %]
+                                            [% IF ( branch.branchcode == request.branchcode ) %]
+                                                <option value="[% branch.branchcode %]" selected="selected">
+                                                    [% branch.branchname %]
+                                                </option>
+                                            [% ELSE %]
+                                                <option value="[% branch.branchcode %]">
+                                                    [% branch.branchname %]
+                                                </option>
+                                            [% END %]
+                                        [% END %]
+                                    </select>
+                                </li>
+                                <li class="status">
+                                    <label class="status">Status:</label>
+                                    [% stat = request.status %]
+                                    [% request.capabilities.$stat.name %]
+                                </li>
+                                <li class="updated">
+                                    <label class="updated">Last updated:</label>
+                                    [% request.updated %]
+                                </li>
+                                <li class="medium">
+                                    <label class="medium">Request type:</label>
+                                    [% request.medium %]
+                                </li>
+                                <li class="cost">
+                                    <label class="cost">Cost:</label>
+                                    [% request.cost %]
+                                </li>
+                                <li class="req_id">
+                                    <label class="req_id">Request number:</label>
+                                    [% request.id_prefix _ request.illrequest_id %]
+                                </li>
+                                <li class="notesstaff">
+                                    <label for="notesstaff" class="notesstaff">Staff notes:</label>
+                                    <textarea name="notesstaff" id="notesstaff" rows="5">[% request.notesstaff %]</textarea>
+                                </li>
+                                <li class="notesopac">
+                                    <label for="notesopac" class="notesopac">Opac notes:</label>
+                                    <textarea name="notesopac" id="notesopac" rows="5">[% request.notesopac %]</textarea>
+                                </li>
+                            </ol>
+                        </fieldset>
+                        <fieldset class="action">
+                            <input type="hidden" value="edit_action" name="method">
+                            <input type="hidden" value="form" name="stage">
+                            <input type="hidden" value="[% request.illrequest_id %]" name="illrequest_id">
+                            <input type="submit" value="Submit">
+                            <a class="cancel" href="/cgi-bin/koha/ill/ill-requests.pl?method=illview&amp;illrequest_id=[% request.id %]">Cancel</a>
+                        </fieldset>
+                    </form>
+
+                [% ELSIF query_type == 'delete_confirm' %]
+
+                    <div class="dialog alert">
+                        <h3>Are you sure you wish to delete this request?</h3>
+                        <p>
+                            <a class="btn btn-default btn-sm approve" href="?method=delete&amp;illrequest_id=[% request.id %]&amp;confirmed=1"><i class="fa fa-fw fa-check"></i>Yes</a>
+                            <a class="btn btn-default btn-sm deny" href="?method=illview&amp;illrequest_id=[% request.id %]"><i class="fa fa-fw fa-remove"></i>No</a>
+                        </p>
+                    </div>
+
+
+                [% ELSIF query_type == 'illview' %]
+                    [% actions = request.available_actions %]
+                    [% capabilities = request.capabilities %]
+                    [% req_status = request.status %]
+                    <h1>Manage ILL request</h1>
+                    <div id="toolbar" class="btn-toolbar">
+                        <a title="Edit request" id="ill-toolbar-btn-edit-action" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=edit_action&amp;illrequest_id=[% request.illrequest_id %]">
+                        <span class="fa fa-pencil"></span>
+                        Edit request
+                        </a>
+                        [% FOREACH action IN actions %]
+                            [% IF action.method != 0 %]
+                                <a title="[% action.ui_method_name %]" id="ill-toolbar-btn-[% action.id | lower %]" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=[% action.method %]&amp;illrequest_id=[% request.illrequest_id %]">
+                                <span class="fa [% action.ui_method_icon %]"></span>
+                                [% action.ui_method_name %]
+                                </a>
+                            [% END %]
+                        [% END %]
+                    </div>
+                    <div id="ill-view-panel" class="panel panel-default">
+                        <div class="panel-heading">
+                            <h3>Request details</h3>
+                        </div>
+                        <div class="panel-body">
+                            <h4>Details from library</h4>
+                            <div class="rows">
+                                <div class="orderid">
+                                    <span class="label orderid">Order ID:</span>
+                                    [% request.orderid || "N/A" %]
+                                </div>
+                                <div class="borrowernumber">
+                                    <span class="label borrowernumber">Borrower:</span>
+                                    [% borrowerlink = "/cgi-bin/koha/members/moremember.pl"
+                                    _ "?borrowernumber=" _ request.patron.borrowernumber %]
+                                    <a href="[% borrowerlink %]" title="View borrower details">
+                                    [% request.patron.firstname _ " "
+                                    _ request.patron.surname _ " ["
+                                    _ request.patron.cardnumber
+                                    _ "]" %]
+                                    </a>
+                                </div>
+
+                                <div class="biblio_id">
+                                    <span class="label biblio_id">Biblio number:</span>
+                                    [% request.biblio_id || "N/A" %]
+                                </div>
+                                <div class="branchcode">
+                                    <span class="label branchcode">Branch:</span>
+                                    [% Branches.GetName(request.branchcode) %]
+                                </div>
+                                <div class="status">
+                                    <span class="label status">Status:</span>
+                                    [% capabilities.$req_status.name %]
+                                </div>
+                                <div class="updated">
+                                    <span class="label updated">Last updated:</span>
+                                    [% request.updated %]
+                                </div>
+                                <div class="medium">
+                                    <span class="label medium">Request type:</span>
+                                    [% request.medium %]
+                                </div>
+                                <div class="cost">
+                                    <span class="label cost">Cost:</span>
+                                    [% request.cost || "N/A" %]
+                                </div>
+                                <div class="req_id">
+                                    <span class="label req_id">Request number:</span>
+                                    [% request.id_prefix _ request.illrequest_id %]
+                                </div>
+                                <div class="notesstaff">
+                                    <span class="label notes_staff">Staff notes:</span>
+                                    <pre>[% request.notesstaff %]</pre>
+                                </div>
+                                <div class="notesopac">
+                                    <span class="label notes_opac">Notes:</span>
+                                    <pre>[% request.notesopac %]</pre>
+                                </div>
+                            </div>
+                            <div class="rows">
+                                <h4>Details from supplier ([% request.backend %])</h4>
+                                [% FOREACH meta IN request.metadata %]
+                                    <div class="requestmeta-[% meta.key %]">
+                                        <span class="label">[% meta.key %]:</span>
+                                        [% meta.value %]
+                                    </div>
+                                [% END %]
+                            </div>
+                            <div class="rows">
+                                <h3><a id="toggle_requestattributes" href="#">Toggle full supplier metadata</a></h3>
+                                <div id="requestattributes" class="content_hidden">
+                                    [% FOREACH attr IN request.illrequestattributes %]
+                                        <div class="requestattr-[% attr.type %]">
+                                            <span class="label">[% attr.type %]:</span>
+                                            [% attr.value %]
+                                        </div>
+                                    [% END %]
+                                </div>
+
+                            </div>
+                        </div>
+                    </div>
+
+                [% ELSIF query_type == 'illlist' %]
+                    <!-- illlist -->
+                    <h1>View ILL requests</h1>
+                    <div id="results">
+                        <h3>Details for all requests</h3>
+                        <table
+                            [% FOREACH filter IN prefilters %]
+                            data-filter-[% filter.name %]="[% filter.value %]"
+                            [% END %]
+                            id="ill-requests">
+                            <thead>
+                                <tr>
+                                    <th id="orderid">Order ID:</th>
+                                    <th id="author">Author:</th>
+                                    <th id="title">Title:</th>
+                                    <th id="borrower_name">Borrower:</th>
+                                    <th id="borrowernumber">Borrower number:</th>
+                                    <th id="biblio_id">Biblio number:</th>
+                                    <th id="branchcode">Branch:</th>
+                                    <th id="status">Status:</th>
+                                    <th id="updated">Last updated:</th>
+                                    <th id="medium">Request type:</th>
+                                    <th id="cost">Cost:</th>
+                                    <th id="req_id">Request number:</th>
+                                    <th id="link">Manage request:</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                            </tbody>
+                        </table>
+                    </div>
+                [% ELSE %]
+                <!-- Custom Backend Action -->
+                [% INCLUDE $whole.template %]
+
+                [% END %]
+            </div>
+        </div>
+    </div>
+</div>
+
+[% TRY %]
+[% PROCESS backend_jsinclude %]
+[% CATCH %]
+[% END %]
+
+[% INCLUDE 'intranet-bottom.inc' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt
index 2a69dd9..32a1409 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt
@@ -63,6 +63,11 @@
                     <li>
                         <a class="icon_general icon_authorities" href="/cgi-bin/koha/authorities/authorities-home.pl">Authorities</a>
                     </li>
+                    [% IF Koha.Preference('ILLModule') %]
+                    <li>
+                        <a class="icon_general icon_ill" href="/cgi-bin/koha/ill/ill-requests.pl">ILL requests</a>
+                    </li>
+                    [% END %]
                 </ul>
             </div><!-- /area-list-left -->
         </div><!-- /yui-u first -->
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc
index 9afa331..7d27f22 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc
@@ -104,6 +104,15 @@
                 [% END %]
                 <a href="/cgi-bin/koha/opac-discharge.pl">ask for a discharge</a></li>
             [% END %]
+
+            [% IF Koha.Preference( 'ILLModule' ) == 1 %]
+                [% IF ( illrequestsview ) %]
+                    <li class="active">
+                [% ELSE %]
+                    <li>
+                [% END %]
+                <a href="/cgi-bin/koha/opac-illrequests.pl">your interlibrary loan requests</a></li>
+            [% END %]
         </ul>
     </div>
 [% END %]
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt
new file mode 100644
index 0000000..0c4600f
--- /dev/null
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt
@@ -0,0 +1,219 @@
+[% USE Koha %]
+[% USE Branches %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo;   Your Interlibrary loan requests</title>[% INCLUDE 'doc-head-close.inc' %]
+[% BLOCK cssinclude %][% END %]
+</head>
+[% INCLUDE 'bodytag.inc' bodyid='opac-illrequests' bodyclass='scrollto' %]
+[% BLOCK messages %]
+    [% IF message == "1" %]
+        <div class="alert alert-success" role="alert">Request updated</div>
+    [% ELSIF message == "2" %]
+        <div class="alert alert-success" role="alert">Request placed</div>
+    [% END %]
+[% END %]
+[% INCLUDE 'masthead.inc' %]
+<div class="main">
+    <ul class="breadcrumb noprint">
+        <li><a href="/cgi-bin/koha/opac-main.pl">Home</a> <span class="divider">&rsaquo;</span></li>
+        [% IF ( loggedinusername ) %]
+            <li><a href="/cgi-bin/koha/opac-user.pl">[% USER_INFO.title %] [% USER_INFO.firstname %] [% USER_INFO.surname %]</a> <span class="divider">&rsaquo;</span></li>
+        [% END %]
+
+        [% IF method != 'list' %]
+            <li><a href="/cgi-bin/koha/opac-illrequests.pl">Interlibrary loan requests</a> <span class="divider">&rsaquo;</span></li>
+            [% IF method == 'create' %]
+                <li>New Interlibrary loan request</li>
+            [% ELSIF method == 'view' %]
+                <li>View Interlibrary loan request</li>
+            [% END %]
+        [% ELSE %]
+            <li>Interlibrary loan requests</li>
+        [% END %]
+
+    </ul> <!-- / .breadcrumb -->
+
+<div class="container-fluid">
+    <div class="row-fluid">
+        [% IF ( OpacNav||loggedinusername ) && !print %]
+            <div class="span2">
+                <div id="navigation">
+                    [% INCLUDE 'navigation.inc' IsPatronPage=1 %]
+                </div>
+            </div>
+        [% END %]
+
+        [% IF ( OpacNav||loggedinusername ) %]
+            <div class="span10">
+        [% ELSE %]
+            <div class="span12">
+        [% END %]
+            <div id="illrequests" class="maincontent">
+                [% IF method == 'create' %]
+                    <h2>New Interlibrary loan request</h2>
+                    [% INCLUDE messages %]
+                    [% IF backends %]
+                        <form method="post" id="illrequestcreate-form" novalidate="novalidate">
+                            <fieldset class="rows">
+                                <label for="backend">Provider:</label>
+                                <select name="backend">
+                                    [% FOREACH backend IN backends %]
+                                        <option value="[% backend %]">[% backend %]</option>
+                                    [% END %]
+                                </select>
+                            </fieldset>
+                            <fieldset class="action">
+                                <input type="hidden" name="method" value="create">
+                                <input type="submit" name="create_select_backend" value="Next &raquo;">
+                            </fieldset>
+                        </form>
+                    [% ELSE %]
+                        [% PROCESS $whole.opac_template %]
+                    [% END %]
+                [% ELSIF method == 'list' %]
+                    <h2>Interlibrary loan requests</h2>
+                    [% INCLUDE messages %]
+
+                    <div id="illrequests-create-button" class="dropdown btn-group">
+                        [% IF backends.size > 1 %]
+                                <button class="btn btn-default dropdown-toggle" type="button" id="ill-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                                    <i class="fa fa-plus"></i> Create a new request <span class="caret"></span>
+                                </button>
+                                <ul id="backend-dropdown-options" class="dropdown-menu nojs" aria-labelledby="ill-backend-dropdown">
+                                    [% FOREACH backend IN backends %]
+                                        <li><a href="/cgi-bin/koha/opac-illrequests.pl?method=create&amp;backend=[% backend %]">[% backend %]</a></li>
+                                    [% END %]
+                                </ul>
+                        [% ELSE %]
+                            <a id="ill-new" class="btn btn-default" href="/cgi-bin/koha/opac-illrequests.pl?method=create&amp;backend=[% backends.0 %]">
+                                <i class="fa fa-plus"></i> Create a new request
+                            </a>
+                        [% END %]
+                    </div>
+
+                    <table id="illrequestlist" class="table table-bordered table-striped">
+                        <thead>
+                            <tr>
+                                <th>Author</th>
+                                <th>Title</th>
+                                <th>Requested from</th>
+                                <th>Request type</th>
+                                <th>Status</th>
+                                <th>Last updated</th>
+                                <th></th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            [% FOREACH request IN requests %]
+                                [% status = request.status %]
+                                <tr>
+                                    <td>[% request.metadata.Author || 'N/A' %]</td>
+                                    <td>[% request.metadata.Title || 'N/A' %]</td>
+                                    <td>[% request.backend %]</td>
+                                    <td>[% request.medium %]</td>
+                                    <td>[% request.capabilities.$status.name %]</td>
+                                    <td>[% request.updated %]</td>
+                                    <td>
+                                        <a href="/cgi-bin/koha/opac-illrequests.pl?method=view&amp;illrequest_id=[% request.id %]" class="btn btn-default btn-small pull-right">View</a>
+                                    </td>
+                                </tr>
+                            [% END %]
+                        </tbody>
+                    </table>
+                [% ELSIF method == 'view' %]
+                    <h2>View Interlibrary loan request</h2>
+                    [% INCLUDE messages %]
+                    [% status = request.status %]
+                    <form method="post" action="?method=update" id="illrequestupdate-form" novalidate="novalidate">
+                            <fieldset class="rows">
+                                <legend id="library_legend">Details from library</legend>
+                                <ol>
+                                    <li>
+                                        <label for="backend">Requested from:</label>
+                                        [% request.backend %]
+                                    </li>
+                                    [% IF request.biblio_id %]
+                                        <li>
+                                            <label for="biblio">Requested item:</label>
+                                            <a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% request.biblio_id %]">Click here to view</a>
+                                        </li>
+                                    [% END %]
+                                    <li>
+                                        <label for="branchcode">Collection library:</label>
+                                        [% Branches.GetName(request.branchcode) %]
+                                    </li>
+                                    <li>
+                                        <label for="status">Status:</label>
+                                        [% request.capabilities.$status.name %]
+                                    </li>
+                                    <li>
+                                        <label for="medium">Request type:</label>
+                                        [% request.medium %]
+                                    </li>
+                                    <li>
+                                        <label for="placed">Request placed:</label>
+                                        [% request.placed %]
+                                    </li>
+                                    <li>
+                                        <label for="updated">Last updated:</label>
+                                        [% request.updated %]
+                                    </li>
+                                    <li>
+                                        <label for="notesopac">Notes:</label>
+                                        [% IF !request.completed %]
+                                            <textarea name="notesopac" rows="5" cols="50">[% request.notesopac %]</textarea>
+                                        [% ELSE %]
+                                            [% request.notesopac %]
+                                        [% END %]
+                                    </li>
+                                </ol>
+                            </fieldset>
+                            <div class="rows">
+                                <legend id="backend_legend">Details from [% request.backend %]</legend>
+                                [% FOREACH meta IN request.metadata %]
+                                    <div class="requestattr-[% meta.key %]">
+                                        <span class="label">[% meta.key %]:</span>
+                                        [% meta.value || 'N/A' %]
+                                    </div>
+                                [% END %]
+                            </div>
+                            <fieldset class="action illrequest-actions">
+                                <input type="hidden" name="illrequest_id" value="[% request.illrequest_id %]">
+                                <input type="hidden" name="method" value="update">
+                                [% IF !request.completed %]
+                                    [% IF request.status != "CANCREQ" %]
+                                        <a class="cancel-illrequest btn btn-danger" href="/cgi-bin/koha/opac-illrequests.pl?method=cancreq&amp;illrequest_id=[% request.illrequest_id %]">Request cancellation</a>
+                                    [% END %]
+                                    <input type="submit" class="update-illrequest btn btn-default" value="Submit modifications">
+                                [% END %]
+                                <span class="cancel"><a href="/cgi-bin/koha/opac-illrequests.pl">Cancel</a></span>
+                            </fieldset>
+                        </form>
+                    [% END %]
+                </div> <!-- / .maincontent -->
+            </div> <!-- / .span10/12 -->
+        </div> <!-- / .row-fluid -->
+    </div> <!-- / .container-fluid -->
+</div> <!-- / .main -->
+
+[% INCLUDE 'opac-bottom.inc' %]
+
+[% BLOCK jsinclude %]
+[% INCLUDE 'datatables.inc' %]
+<script type="text/javascript">
+    //<![CDATA[
+        $("#illrequestlist").dataTable($.extend(true, {}, dataTablesDefaults, {
+            "aoColumnDefs": [
+                { "aTargets": [ -1 ], "bSortable": false, "bSearchable": false }
+            ],
+            "aaSorting": [[ 3, "desc" ]],
+            "deferRender": true
+        }));
+        $("#backend-dropdown-options").removeClass("nojs");
+    //]]>
+</script>
+[% TRY %]
+[% PROCESS backend_jsinclude %]
+[% CATCH %]
+[% END %]
+[% END %]
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt
index 8595f9a..eaa20b3 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt
@@ -253,14 +253,30 @@ href="/cgi-bin/koha/opac-rss.pl?[% query_cgi %][% limit_cgi |html %]" />
                             [% INCLUDE 'page-numbers.inc' %]
                         [% END # / IF total %]
 
-                        [% IF Koha.Preference( 'suggestion' ) == 1 %]
-                            [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
-                                <div class="suggestion">Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>
-                            [% ELSE %]
-                                [% IF ( loggedinusername ) %]<div class="suggestion">Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>[% END %]
-                            [% END %]
+                        [% IF
+                            Koha.Preference( 'suggestion' ) == 1 &&
+                            (
+                                Koha.Preference( 'AnonSuggestions' ) == 1 ||
+                                loggedinusername ||
+                                Koha.Preference( 'ILLModule' ) == 1
+                            )
+                        %]
+                            <div class="suggestion">
+                                Not finding what you're looking for?
+                                <ul>
+                                    [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
+                                        <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                    [% ELSE %]
+                                        [% IF ( loggedinusername ) %]
+                                            <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                        [% END %]
+                                    [% END %]
+                                    [% IF Koha.Preference( 'ILLModule' ) == 1 && loggedinusername %]
+                                        <li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create">Interlibrary loan request</a></li>
+                                    [% END %]
+                                </ul>
+                            </div>
                         [% END %]
-
                     </div> <!-- / #grouped-results -->
                 </div> <!-- /.span10/12 -->
             </div> <!-- / .row-fluid -->
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
index a5a7c70..11cf98e 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
@@ -562,16 +562,29 @@
 
                     [% END # / IF total %]
 
-                    [% IF Koha.Preference( 'suggestion' ) == 1 %]
-                        [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
-                            <div class="suggestion">Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>
-                        [% ELSE %]
-                            [% IF ( loggedinusername ) %]
-                                <div class="suggestion">
-                                    Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a>
-                                </div>
-                            [% END %]
-                        [% END %]
+                    [% IF
+                        Koha.Preference( 'suggestion' ) == 1 &&
+                        (
+                            Koha.Preference( 'AnonSuggestions' ) == 1 ||
+                            loggedinusername ||
+                            Koha.Preference( 'ILLModule' ) == 1
+                        )
+                    %]
+                        <div class="suggestion">
+                            Not finding what you're looking for?
+                            <ul>
+                                [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
+                                    <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                [% ELSE %]
+                                    [% IF ( loggedinusername ) %]
+                                        <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                    [% END %]
+                                [% END %]
+                                [% IF Koha.Preference( 'ILLModule' ) == 1 && loggedinusername %]
+                                    <li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create">Interlibrary loan request</a></li>
+                                [% END %]
+                            </ul>
+                        </div>
                     [% END %]
                     </div> <!-- / #userresults -->
                 </div> <!-- /.span10/12 -->
diff --git a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
index 63961e8..c4e55c2 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
+++ b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
@@ -2508,6 +2508,44 @@ a.reviewlink:visited {
     font-size: 90%;
 }
 
+#illrequests {
+    .illrequest-actions {
+        .btn,
+        .cancel {
+            margin-right: 5px;
+        }
+        padding-top: 20px;
+        margin-bottom: 20px;
+    }
+    #illrequests-create-button {
+        margin-bottom: 20px;
+    }
+    .bg-info {
+        overflow: auto;
+        position: relative;
+    }
+    .bg-info {
+        #search-summary {
+            -webkit-transform: translateY(-50%);
+            -ms-transform: translateY(-50%);
+            -o-transform: translateY(-50%);
+            transform: translateY(-50%);
+            position: absolute;
+            top: 50%;
+        }
+
+    }
+    #freeform-fields .custom-name {
+        float: left;
+        width: 8em;
+        margin-right: 1em;
+        text-align: right;
+    }
+    .dropdown:hover .dropdown-menu.nojs {
+        display: block;
+    }
+}
+
 #dc_fieldset {
     border: 1px solid #dddddd;
     border-width: 1px;
diff --git a/opac/opac-illrequests.pl b/opac/opac-illrequests.pl
new file mode 100755
index 0000000..9ff413a
--- /dev/null
+++ b/opac/opac-illrequests.pl
@@ -0,0 +1,129 @@
+#!/usr/bin/perl
+
+# Copyright 2017 PTFS-Europe Ltd
+#
+# 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 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use CGI qw ( -utf8 );
+use C4::Auth;
+use C4::Koha;
+use C4::Output;
+
+use Koha::Illrequests;
+use Koha::Libraries;
+use Koha::Patrons;
+
+my $query = new CGI;
+
+# Grab all passed data
+# 'our' since Plack changes the scoping
+# of 'my'
+our $params = $query->Vars();
+
+# if illrequests is disabled, leave immediately
+if ( ! C4::Context->preference('ILLModule') ) {
+    print $query->redirect("/cgi-bin/koha/errors/404.pl");
+    exit;
+}
+
+my ( $template, $loggedinuser, $cookie ) = get_template_and_user({
+    template_name   => "opac-illrequests.tt",
+    query           => $query,
+    type            => "opac",
+    authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
+});
+
+my $op = $params->{'method'} || 'list';
+
+if ( $op eq 'list' ) {
+
+    my $requests = Koha::Illrequests->search(
+        { borrowernumber => $loggedinuser }
+    );
+    my $req = Koha::Illrequest->new;
+    $template->param(
+        requests => $requests,
+        backends    => $req->available_backends
+    );
+
+} elsif ( $op eq 'view') {
+    my $request = Koha::Illrequests->find({
+        borrowernumber => $loggedinuser,
+        illrequest_id  => $params->{illrequest_id}
+    });
+    $template->param(
+        request => $request
+    );
+
+} elsif ( $op eq 'update') {
+    my $request = Koha::Illrequests->find({
+        borrowernumber => $loggedinuser,
+        illrequest_id  => $params->{illrequest_id}
+    });
+    $request->notesopac($params->{notesopac})->store;
+    print $query->redirect(
+        '/cgi-bin/koha/opac-illrequests.pl?method=view&illrequest_id=' .
+        $params->{illrequest_id} .
+        '&message=1'
+    );
+} elsif ( $op eq 'cancreq') {
+    my $request = Koha::Illrequests->find({
+        borrowernumber => $loggedinuser,
+        illrequest_id  => $params->{illrequest_id}
+    });
+    $request->status('CANCREQ')->store;
+    print $query->redirect(
+        '/cgi-bin/koha/opac-illrequests.pl?method=view&illrequest_id=' .
+        $params->{illrequest_id} .
+        '&message=1'
+    );
+
+} elsif ( $op eq 'create' ) {
+    if (!$params->{backend}) {
+        my $req = Koha::Illrequest->new;
+        $template->param(
+            backends    => $req->available_backends
+        );
+    } else {
+        my $request = Koha::Illrequest->new
+            ->load_backend($params->{backend});
+        $params->{cardnumber} = Koha::Patrons->find({
+            borrowernumber => $loggedinuser
+        })->cardnumber;
+        my $backend_result = $request->backend_create($params);
+        $template->param(
+            media       => [ "Book", "Article", "Journal" ],
+            branches    => Koha::Libraries->search->unblessed,
+            whole       => $backend_result,
+            request     => $request
+        );
+        if ($backend_result->{stage} eq 'commit') {
+            print $query->redirect('/cgi-bin/koha/opac-illrequests.pl?message=2');
+        }
+    }
+
+
+}
+
+$template->param(
+    message         => $params->{message},
+    illrequestsview => 1,
+    method              => $op
+);
+
+output_html_with_http_headers $query, $cookie, $template->output;
diff --git a/t/db_dependent/Illrequest.t b/t/db_dependent/Illrequest.t
new file mode 100644
index 0000000..5fd5f0b
--- /dev/null
+++ b/t/db_dependent/Illrequest.t
@@ -0,0 +1,544 @@
+#!/usr/bin/perl
+#
+# 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 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
+#
+
+use Modern::Perl;
+
+use File::Basename qw/basename/;
+use Koha::Database;
+use Koha::Illrequestattributes;
+use Koha::Patrons;
+use t::lib::TestBuilder;
+
+use Test::More tests => 44;
+
+# We want to test the Koha IllRequest object.  At its core it's a simple
+# Koha::Object, mapping to the ill_request table.
+#
+# This object will supersede the Status object in ILLModule.
+#
+# We must ensure perfect backward compatibility between the current model and
+# the Status less model.
+
+use_ok('Koha::Illrequest');
+use_ok('Koha::Illrequests');
+
+my $schema = Koha::Database->new->schema;
+$schema->storage->txn_begin;
+
+my $builder = t::lib::TestBuilder->new;
+
+my $patron = $builder->build({ source => 'Borrower' });
+my $branch = $builder->build({ source => 'Branch' });
+
+my $illRequest = $builder->build({
+    source => 'Illrequest',
+    value => {
+        borrowernumber  => $patron->{borrowernumber},
+        branch          => $branch->{branchcode},
+        biblionumber    => 0,
+        status          => 'NEW',
+        completion_date => 0,
+        reqtype         => 'book',
+    }
+});
+
+my $illObject = Koha::Illrequests->find($illRequest->{illrequest_id});
+
+isa_ok($illObject, "Koha::Illrequest");
+
+# Test delete works correctly.
+my $illRequestDelete = $builder->build({
+    source => 'Illrequest',
+    value => {
+        borrowernumber  => $patron->{borrowernumber},
+        branch          => $branch->{branchcode},
+        biblionumber    => 0,
+        status          => 'NEW',
+        completion_date => 0,
+        reqtype         => 'book',
+    }
+});
+sub ill_req_search {
+    return Koha::Illrequestattributes->search({
+        illrequest_id => $illRequestDelete->{illrequest_id}
+    })->count;
+}
+
+is(ill_req_search, 0, "Correctly not found matching Illrequestattributes.");
+# XXX: For some reason test builder can't build Illrequestattributes.
+my $illReqAttr = Koha::Illrequestattribute->new({
+    illrequest_id => $illRequestDelete->{illrequest_id},
+    type => "test",
+    value => "Hello World"
+})->store;
+is(ill_req_search, 1, "We have found a matching Illrequestattribute.");
+
+Koha::Illrequests->find($illRequestDelete->{illrequest_id})->delete;
+is(
+    Koha::Illrequests->find($illRequestDelete->{illrequest_id}),
+    undef,
+    "Correctly deleted Illrequest."
+);
+is(ill_req_search, 0, "Correctly deleted Illrequestattributes.");
+
+# Test Accessing of related records.
+
+# # TODO the conclusion from being able to use one_to_many? we no longer need
+# # the Record object: simply pass the `ill_request_attributes` resultset
+# # whenever one would pass a Record.
+
+my $illRequest2 = $builder->build({
+    source => 'Illrequest',
+    value  => {
+        borrower_id => $patron->{borrowernumber},
+        branch_id   => $branch->{branchcode},
+        biblio_id   => 0,
+        status      => 'NEW',
+        completed   => 0,
+        medium      => 'book',
+    }
+});
+my $illReqAttr2 = Koha::Illrequestattribute->new({
+    illrequest_id => $illRequest2->{illrequest_id},
+    type          => "test2",
+    value         => "Hello World"
+})->store;
+my $illReqAttr3 = Koha::Illrequestattribute->new({
+    illrequest_id => $illRequest2->{illrequest_id},
+    type          => "test3",
+    value         => "Hello Space"
+})->store;
+
+my $illRequestAttributes = Koha::Illrequests
+    ->find($illRequest2->{illrequest_id})->illrequestattributes;
+
+isa_ok($illRequestAttributes, "Koha::Illrequestattributes");
+
+is($illRequestAttributes->count, 2, "Able to search related.");
+
+# Test loading of 'Config'.
+
+my $rqConfigTest = Koha::Illrequest->new({
+    borrower_id => $patron->{borrowernumber},
+    branch_id   => $branch->{branchcode},
+});
+
+isa_ok($rqConfigTest->_config, "Koha::Illrequest::Config");
+
+# Test loading of 'Dummy' backend.
+
+my $rqBackendTest = Koha::Illrequest->new({
+    borrower_id => $patron->{borrowernumber},
+    branch_id   => $branch->{branchcode},
+})->store;
+
+$rqBackendTest->_config->backend("Dummy");
+$rqBackendTest->_config->limits({ default => { count => -1 } });
+isa_ok($rqBackendTest->_backend, "Koha::Illbackends::Dummy::Base");
+
+# Test use of 'Dummy' Backend.
+
+## Test backend_update_status
+
+# FIXME: This breaks transparancy of ->status method!
+eval { $rqBackendTest->status("ERR") };
+ok($@, "status: Test for status error on hook fail.");
+
+# FIXME: Will need to test this on new illRequest to not pollute rest of
+# tests.
+
+# is($rqBackendTest->status("NEW")->status, "NEW", "status: Setter works
+# OK.");
+# is($rqBackendTest->status(null), null, "status: Unsetter works OK.");
+
+## Test backend_create
+
+is(
+    $rqBackendTest->status,
+    undef,
+    "backend_create: Test our status initiates correctly."
+);
+
+# Request a search form
+my $created_rq = $rqBackendTest->backend_create({
+    stage  => "search_form",
+    method => "create",
+});
+
+is( $created_rq->{stage}, 'search_results',
+    "backend_create: search_results stage." );
+
+# Request search results
+# FIXME: fails because of missing patron / branch info.
+# $created_rq = $rqBackendTest->backend_create({
+#     stage  => "search_results",
+#     method => "create",
+#     other  => { search => "interlibrary loans" },
+# });
+
+# is_deeply(
+#     $created_rq,
+#     {
+#         error    => 0,
+#         status   => '',
+#         message  => '',
+#         method   => 'create',
+#         stage    => 'search_results',
+#         template => 'ill/Dummy/create.inc',
+#         value    => [
+#             {
+#                 id     => 1234,
+#                 title  => "Ordering ILLs using Koha",
+#                 author => "A.N. Other",
+#             },
+#             {
+#                 id     => 5678,
+#                 title  => "Interlibrary loans in Koha",
+#                 author => "A.N. Other",
+#             },
+#         ],
+#     }
+#     ,
+#     "backend_create: search_results stage."
+# );
+
+# # Create the request
+# $created_rq = $rqBackendTest->backend_create({
+#     stage  => "commit",
+#     method => "create",
+#     other  => {
+#         id     => 1234,
+#         title  => "Ordering ILLs using Koha",
+#         author => "A.N. Other",
+#     },
+# });
+
+# while ( my ( $field, $value ) = each %{$created_rq} ) {
+#     isnt($value, undef, "backend_create: key '$field' exists");
+# };
+
+# is(
+#     $rqBackendTest->status,
+#     "NEW",
+#     "backend_create: Test our status was updated."
+# );
+
+# cmp_ok(
+#     $rqBackendTest->illrequestattributes->count,
+#     "==",
+#     3,
+#     "backend_create: Ensure we have correctly stored our new attributes."
+# );
+
+# ## Test backend_list
+
+# is_deeply(
+#     $rqBackendTest->backend_list->{value},
+#     {
+#         1 => {
+#             id     => 1234,
+#             title  => "Ordering ILLs using Koha",
+#             author => "A.N. Other",
+#             status => "On order",
+#             cost   => "30 GBP",
+#         }
+#     },
+#     "backend_list: Retrieve our list of requested requests."
+# );
+
+# ## Test backend_renew
+
+# ok(
+#     $rqBackendTest->backend_renew->{error},
+#     "backend_renew: Error for invalid request."
+# );
+# is_deeply(
+#     $rqBackendTest->backend_renew->{value},
+#     {
+#         id     => 1234,
+#         title  => "Ordering ILLs using Koha",
+#         author => "A.N. Other",
+#     },
+#     "backend_renew: Renew request."
+# );
+
+# ## Test backend_confirm
+
+# my $rqBackendTestConfirmed = $rqBackendTest->backend_confirm;
+# is(
+#     $rqBackendTest->status,
+#     "REQ",
+#     "backend_commit: Confirm status update correct."
+# );
+# is(
+#     $rqBackendTest->orderid,
+#     1234,
+#     "backend_commit: Confirm orderid populated correctly."
+# );
+
+# ## Test backend_status
+
+# is(
+#     $rqBackendTest->backend_status->{error},
+#     0,
+#     "backend_status: error for invalid request."
+# );
+# is_deeply(
+#     $rqBackendTest->backend_status->{value},
+#     {
+#         id     => 1234,
+#         status => "On order",
+#         title  => "Ordering ILLs using Koha",
+#         author => "A.N. Other",
+#     },
+#     "backend_status: Retrieve the status of request."
+# );
+
+# # Now test trying to get status on non-confirmed request.
+my $rqBackendTestUnconfirmed = Koha::Illrequest->new({
+    borrower_id => $patron->{borrowernumber},
+    branch_id   => $branch->{branchcode},
+})->store;
+$rqBackendTestUnconfirmed->_config->backend("Dummy");
+$rqBackendTestUnconfirmed->_config->limits({ default => { count => -1 } });
+
+$rqBackendTestUnconfirmed->backend_create({
+    stage  => "commit",
+    method => "create",
+    other  => {
+        id     => 1234,
+        title  => "Ordering ILLs using Koha",
+        author => "A.N. Other",
+    },
+});
+is(
+    $rqBackendTestUnconfirmed->backend_status->{error},
+    1,
+    "backend_status: error for invalid request."
+);
+
+## Test backend_cancel
+
+# is(
+#     $rqBackendTest->backend_cancel->{error},
+#     0,
+#     "backend_cancel: Successfully cancelling request."
+# );
+# is_deeply(
+#     $rqBackendTest->backend_cancel->{value},
+#     {
+#         id     => 1234,
+#         title  => "Ordering ILLs using Koha",
+#         author => "A.N. Other",
+#     },
+#     "backend_cancel: Cancel request."
+# );
+
+# Now test trying to cancel non-confirmed request.
+is(
+    $rqBackendTestUnconfirmed->backend_cancel->{error},
+    1,
+    "backend_cancel: error for invalid request."
+);
+is_deeply(
+    $rqBackendTestUnconfirmed->backend_cancel->{value},
+    {},
+    "backend_cancel: Cancel request."
+);
+
+# Test Helpers
+
+## Test getCensorNotesStaff
+
+is($rqBackendTest->getCensorNotesStaff, 1, "getCensorNotesStaff: Public.");
+$rqBackendTest->_config->censorship({
+    censor_notes_staff => 0,
+    censor_reply_date  => 0,
+});
+is($rqBackendTest->getCensorNotesStaff, 0, "getCensorNotesStaff: Censored.");
+
+## Test getCensorNotesStaff
+
+is($rqBackendTest->getDisplayReplyDate, 1, "getDisplayReplyDate: Yes.");
+$rqBackendTest->_config->censorship({
+    censor_notes_staff => 0,
+    censor_reply_date  => 1,
+});
+is($rqBackendTest->getDisplayReplyDate, 0, "getDisplayReplyDate: No.");
+
+# FIXME: These should be handled by the templates.
+# # Test Output Helpers
+
+# ## Test getStatusSummary
+
+# $rqBackendTest->medium("Book")->store;
+# is_deeply(
+#     $rqBackendTest->getStatusSummary({brw => 0}),
+#     {
+#         biblionumber => ["Biblio Number", undef],
+#         borrowernumber => ["Borrower Number", $patron->{borrowernumber}],
+#         id => ["Request Number", $rqBackendTest->illrequest_id],
+#         prefix_id => ["Request Number", $rqBackendTest->illrequest_id],
+#         reqtype => ["Request Type", "Book"],
+#         status => ["Status", "REQREV"],
+#     },
+#     "getStatusSummary: Without Borrower."
+# );
+
+# is_deeply(
+#     $rqBackendTest->getStatusSummary({brw => 1}),
+#     {
+#         biblionumber => ["Biblio Number", undef],
+#         borrower => ["Borrower", Koha::Patrons->find($patron->{borrowernumber})],
+#         id => ["Request Number", $rqBackendTest->illrequest_id],
+#         prefix_id => ["Request Number", $rqBackendTest->illrequest_id],
+#         reqtype => ["Request Type", "Book"],
+#         status => ["Status", "REQREV"],
+#     },
+#     "getStatusSummary: With Borrower."
+# );
+
+# ## Test getFullStatus
+
+# is_deeply(
+#     $rqBackendTest->getFullStatus({brw => 0}),
+#     {
+#         biblionumber => ["Biblio Number", undef],
+#         borrowernumber => ["Borrower Number", $patron->{borrowernumber}],
+#         id => ["Request Number", $rqBackendTest->illrequest_id],
+#         prefix_id => ["Request Number", $rqBackendTest->illrequest_id],
+#         reqtype => ["Request Type", "Book"],
+#         status => ["Status", "REQREV"],
+#         placement_date => ["Placement Date", $rqBackendTest->placed],
+#         completion_date => ["Completion Date", $rqBackendTest->completed],
+#         ts => ["Timestamp", $rqBackendTest->updated],
+#         branch => ["Branch", $rqBackendTest->branch_id],
+#     },
+#     "getFullStatus: Without Borrower."
+# );
+
+# is_deeply(
+#     $rqBackendTest->getFullStatus({brw => 1}),
+#     {
+#         biblionumber => ["Biblio Number", undef],
+#         borrower => ["Borrower", Koha::Patrons->find($patron->{borrowernumber})],
+#         id => ["Request Number", $rqBackendTest->illrequest_id],
+#         prefix_id => ["Request Number", $rqBackendTest->illrequest_id],
+#         reqtype => ["Request Type", "Book"],
+#         status => ["Status", "REQREV"],
+#         placement_date => ["Placement Date", $rqBackendTest->placed],
+#         completion_date => ["Completion Date", $rqBackendTest->completed],
+#         ts => ["Timestamp", $rqBackendTest->updated],
+#         branch => ["Branch", $rqBackendTest->branch_id],
+#     },
+#     "getFullStatus: With Borrower."
+# );
+
+## Test available_backends
+subtest 'available_backends' => sub {
+    plan tests => 1;
+
+    my $rq = Koha::Illrequest->new({
+        borrower_id => $patron->{borrowernumber},
+        branch_id   => $branch->{branchcode},
+    })->store;
+
+    my @backends = ();
+    my $backenddir = $rq->_config->backend_dir;
+    @backends = <$backenddir/*> if ( $backenddir );
+    @backends = map { basename($_) } @backends;
+    is_deeply(\@backends, $rq->available_backends,
+              "Correctly identify available backends.");
+
+};
+
+## Test capabilities
+
+my $rqCapTest = Koha::Illrequest->new({
+    borrower_id => $patron->{borrowernumber},
+    branch_id   => $branch->{branchcode},
+})->store;
+
+is( keys %{$rqCapTest->_core_status_graph},
+    @{[ 'NEW', 'REQ', 'REVREQ', 'QUEUED', 'CANCREQ', 'COMP', 'KILL' ]},
+    "Complete list of core statuses." );
+
+my $union = $rqCapTest->_status_graph_union(
+    $rqCapTest->_core_status_graph,
+    {
+        TEST => {
+            prev_actions => [ 'COMP' ],
+            id           => 'TEST',
+            name         => "Test",
+            ui_method_name => "Perform test",
+            method         => 'test',
+            next_actions   => [ 'NEW' ]
+        },
+        BLAH => {
+            prev_actions => [ 'COMP' ],
+            id           => 'BLAH',
+            name         => "BLAH",
+            ui_method_name => "Perform test",
+            method         => 'test',
+            next_actions   => [ 'NEW' ]
+        },
+    }
+);
+ok( ( grep 'BLAH', @{$union->{COMP}->{next_actions}} and
+          grep 'TEST', @{$union->{COMP}->{next_actions}} ),
+    "next_actions: updated." );
+ok( ( grep 'BLAH', @{$union->{NEW}->{prev_actions}} and
+          grep 'TEST', @{$union->{NEW}->{prev_actions}} ),
+    "next_actions: updated." );
+
+## Test available_backends
+subtest 'available_actions' => sub {
+    plan tests => 1;
+
+    my $rq = Koha::Illrequest->new({
+        borrower_id => $patron->{borrowernumber},
+        branch_id   => $branch->{branchcode},
+        status      => 'NEW',
+    })->store;
+
+    is_deeply(
+        $rq->available_actions,
+        [
+            {
+                prev_actions   => [ 'NEW', 'REQREV', 'QUEUED' ],
+                id             => 'REQ',
+                name           => 'Requested',
+                ui_method_name => 'Create request',
+                method         => 'confirm',
+                next_actions   => [ 'REQREV' ],
+            },
+            {
+                prev_actions   => [ 'CANCREQ', 'QUEUED', 'REQREV', 'NEW' ],
+                id             => 'KILL',
+                name           => 0,
+                ui_method_name => 'Delete request',
+                method         => 'delete',
+                next_actions   => [ ],
+            }
+        ]
+    );
+};
+
+$schema->storage->txn_rollback;
+
+1;
diff --git a/t/db_dependent/Illrequest/Config.t b/t/db_dependent/Illrequest/Config.t
new file mode 100644
index 0000000..df2bcac
--- /dev/null
+++ b/t/db_dependent/Illrequest/Config.t
@@ -0,0 +1,344 @@
+#!/usr/bin/perl
+
+# 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 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Test::Exception;
+use Test::More;
+use Test::Warn;
+
+# Some data structures that will be repeatedly referenced
+my $defaults  = {
+    api_key  => "564euie",
+    api_auth => "unidaenudvnled",
+};
+my $application = {
+    key  => "6546uedrun",
+    auth => "edutrineadue",
+};
+# Simulate $from_xml
+my $params = {
+    application            => $application,
+    backend                => "Dummy",
+    configuration          => $defaults,
+    reply_date             => "hide",
+    staff_request_comments => "hide",
+};
+my $first_branch = {
+    code => "test", api_key => "dphügnpgüffq", api_auth => "udrend"
+};
+my $second_branch = {
+    code          => "second",
+    api_key       => "eduirn",
+    api_auth      => "eudtireand",
+    request_limit => { count => "5" },
+};
+
+BEGIN {
+    use_ok('Koha::Illrequest::Config');
+}
+
+my $config = Koha::Illrequest::Config->new(1); # with test_mode enabled.
+isa_ok($config, 'Koha::Illrequest::Config');
+
+# _load_configuration
+is_deeply(
+    Koha::Illrequest::Config::_load_configuration($params),
+    {
+        api_url         => 'http://apitest.bldss.bl.uk',
+        backend         => 'Dummy',
+        censorship      => {
+            censor_notes_staff => 1,
+            censor_reply_date => 1,
+        },
+        credentials     => {
+            api_application => $application,
+            api_keys        => { default => $defaults },
+        },
+        default_formats => {},
+        digital_recipients => {},
+        library_privileges => {},
+        limits          => {},
+        prefixes           => {},
+        spec_file       => undef,
+    },
+    "Basic _load_configuration"
+);
+
+$params->{configuration}->{request_limit}->{count} = 10;
+is_deeply(
+    Koha::Illrequest::Config::_load_configuration($params),
+    {
+        api_url            => 'http://apitest.bldss.bl.uk',
+        backend            => 'Dummy',
+        censorship         => {
+            censor_notes_staff => 1,
+            censor_reply_date => 1,
+        },
+        credentials        => {
+            api_application => $application,
+            api_keys        => {
+                default => {
+                    api_auth => $defaults->{api_auth},
+                    api_key  => $defaults->{api_key},
+                }
+            },
+        },
+        default_formats    => {},
+        digital_recipients => {},
+        library_privileges => {},
+        limits             => { default => { count => 10 } },
+        prefixes           => {},
+        spec_file          => undef,
+    },
+    "Basic _load_configuration, with limit"
+);
+
+$params->{configuration}->{branch} = $first_branch;
+is_deeply(
+    Koha::Illrequest::Config::_load_configuration($params),
+    {
+        api_url         => 'http://apitest.bldss.bl.uk',
+        backend         => 'Dummy',
+        censorship      => {
+            censor_notes_staff => 1,
+            censor_reply_date => 1,
+        },
+        credentials     => {
+            api_keys        => {
+                default => {
+                    api_key  => $defaults->{api_key},
+                    api_auth => $defaults->{api_auth},
+                },
+                $first_branch->{code} => {
+                    api_key  => $first_branch->{api_key},
+                    api_auth => $first_branch->{api_auth},
+                },
+            },
+            api_application => $application,
+        },
+        default_formats => {},
+        digital_recipients => {},
+        library_privileges => {},
+        limits          => { default => { count => 10 } },
+        prefixes           => {},
+        spec_file       => undef,
+    },
+    "Single Branch _load_configuration"
+);
+
+$params->{configuration}->{branch} = [ $first_branch, $second_branch ];
+is_deeply(
+    Koha::Illrequest::Config::_load_configuration($params),
+    {
+        api_url         => 'http://apitest.bldss.bl.uk',
+        credentials     => {
+            api_keys        => {
+                default => {
+                    api_key  => $defaults->{api_key},
+                    api_auth => $defaults->{api_auth},
+                },
+                $first_branch->{code} => {
+                    api_key  => $first_branch->{api_key},
+                    api_auth => $first_branch->{api_auth},
+                },
+                $second_branch->{code} => {
+                    api_key  => $second_branch->{api_key},
+                    api_auth => $second_branch->{api_auth},
+                },
+            },
+            api_application => $application,
+        },
+        backend         => 'Dummy',
+        censorship      => {
+            censor_notes_staff => 1,
+            censor_reply_date => 1,
+        },
+        default_formats => {},
+        digital_recipients => {},
+        library_privileges => {},
+        limits          => {
+            default => {count => 10 },
+            branch  => {
+                $second_branch->{code} => {count => 5 },
+            },
+        },
+        prefixes           => {},
+        spec_file       => undef,
+    },
+    "Multi Branch _load_configuration"
+);
+
+dies_ok { Koha::Illrequest::Config::_load_configuration($params, 1) }
+    "Unmediated, missing config _load_configuration";
+
+$params->{configuration}->{default_formats} = {
+    format => 1, quality => 1, quantity => 1, service => 1, speed => 1
+};
+is_deeply(
+    Koha::Illrequest::Config::_load_configuration($params, 1),
+    {
+        api_url         => 'http://apitest.bldss.bl.uk',
+        backend         => 'Dummy',
+        censorship      => {
+            censor_notes_staff => 1,
+            censor_reply_date => 1,
+        },
+        credentials     => {
+            api_keys        => {
+                default => {
+                    api_key  => $defaults->{api_key},
+                    api_auth => $defaults->{api_auth},
+                },
+                $first_branch->{code} => {
+                    api_key  => $first_branch->{api_key},
+                    api_auth => $first_branch->{api_auth},
+                },
+                $second_branch->{code} => {
+                    api_key  => $second_branch->{api_key},
+                    api_auth => $second_branch->{api_auth},
+                },
+            },
+            api_application => $application,
+        },
+        default_formats => {
+            default => {
+                format => 1,
+                quality => 1,
+                quantity => 1,
+                service => 1,
+                speed => 1
+            }
+        },
+        digital_recipients => {},
+        library_privileges => {},
+        limits          => {
+            default => {count => 10 },
+            branch  => {
+                $second_branch->{code} => {count => 5 },
+            },
+        },
+        prefixes           => {},
+        spec_file       => undef,
+    },
+    "default_formats, default only _load_configuration"
+);
+
+# getDefaultFormats
+dies_ok { $config->getLimitRules('wrongType') }
+    "Faulty getDefaultFormats";
+
+$config->{configuration} =
+    Koha::Illrequest::Config::_load_configuration($params);
+is_deeply(
+    $config->getDefaultFormats('brw_cat'),
+    {
+        default => {
+            format => 1,
+            quality => 1,
+            quantity => 1,
+            service => 1,
+            speed => 1
+        }
+    },
+    "Default getDefaultFormats"
+);
+
+# getLimitRules
+dies_ok { $config->getLimitRules('wrongType') }
+    "Faulty getLimitRules";
+
+$config->{configuration} =
+    Koha::Illrequest::Config::_load_configuration($params);
+is_deeply(
+    $config->getLimitRules('branch'),
+    {
+        default => { count => 10 },
+        second  => { count => 5 },
+    },
+    "second branch getLimitRules"
+);
+
+is_deeply(
+    $config->getLimitRules('brw_cat'),
+    {
+        default => { count => 10 },
+    },
+    "empty brw_cat getLimitRules"
+);
+
+# getCredentials
+$params = {
+    application            => $application,
+    backend                => 'Dummy',
+    configuration          => {},
+    reply_date             => "hide",
+    staff_request_comments => "hide",
+};
+$config->{configuration} =
+    Koha::Illrequest::Config::_load_configuration($params);
+is_deeply(
+    $config->getCredentials,
+    {
+        api_key              => 0,
+        api_key_auth         => 0,
+        api_application      => $application->{key},
+        api_application_auth => $application->{auth},
+    },
+    "getCredentials, no creds, just App."
+);
+
+$params->{configuration} = $defaults;
+$config->{configuration} =
+    Koha::Illrequest::Config::_load_configuration($params),
+is_deeply(
+    $config->getCredentials,
+    {
+        api_key              => $defaults->{api_key},
+        api_key_auth         => $defaults->{api_auth},
+        api_application      => $application->{key},
+        api_application_auth => $application->{auth},
+    },
+    "getCredentials, default creds & App."
+);
+
+$params->{configuration}->{branch} = $first_branch;
+$config->{configuration} =
+    Koha::Illrequest::Config::_load_configuration($params),
+is_deeply(
+    $config->getCredentials($first_branch->{code}),
+    {
+        api_key              => $first_branch->{api_key},
+        api_key_auth         => $first_branch->{api_auth},
+        api_application      => $application->{key},
+        api_application_auth => $application->{auth},
+    },
+    "getCredentials, $first_branch->{code} creds & App."
+);
+
+is_deeply(
+    $config->getCredentials("random"),
+    {
+        api_key              => $defaults->{api_key},
+        api_key_auth         => $defaults->{api_auth},
+        api_application      => $application->{key},
+        api_application_auth => $application->{auth},
+    },
+    "getCredentials, fallback creds & app."
+);
+
+done_testing;
diff --git a/t/db_dependent/Illrequest/Dummy.t b/t/db_dependent/Illrequest/Dummy.t
new file mode 100644
index 0000000..38b5fac
--- /dev/null
+++ b/t/db_dependent/Illrequest/Dummy.t
@@ -0,0 +1,378 @@
+#!/usr/bin/perl
+#
+# 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 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
+#
+
+use Modern::Perl;
+
+use Koha::Database;
+use Koha::Illrequests;
+use t::lib::TestBuilder;
+
+use Test::More tests => 15;
+
+# This is a set of basic tests for the Dummy backend, largely to provide
+# sanity checks for testing at the higher level Illrequest.pm level.
+#
+# The Dummy backend is rather simple, but provides correctly formatted
+# response values, that other backends can model themselves after.
+
+use_ok('Koha::Illrequest::Backend::Dummy');
+
+my $backend = Koha::Illrequest::Backend::Dummy->new;
+
+isa_ok($backend, 'Koha::Illrequest::Backend::Dummy');
+
+
+my $schema = Koha::Database->new->schema;
+$schema->storage->txn_begin;
+
+my $builder = t::lib::TestBuilder->new;
+
+my $patron = $builder->build({ source => 'Borrower' });
+my $branch = $builder->build({ source => 'Branch' });
+
+my $illRequest = $builder->build({
+    source => 'Illrequest',
+    value => {
+        borrowernumber  => $patron->{borrowernumber},
+        branch          => $branch->{branchcode},
+    }
+});
+my $mock_request = Koha::Illrequests->find($illRequest->{illrequest_id});
+$mock_request->_config->backend("Dummy");
+$mock_request->_config->limits({ default => { count => -1 } });
+
+# Test Create
+my $rq = $backend->create({
+    request    => $mock_request,
+    method     => 'create',
+    stage      => 'search_form',
+    other      => undef,
+});
+
+is_deeply(
+    $rq,
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'create',
+        stage   => 'search_form',
+        value   => {},
+    },
+    "Search_Form stage of create method."
+);
+
+$rq = $backend->create({
+    request    => $mock_request,
+    method     => 'create',
+    stage      => 'search_results',
+    other      => { search => "interlibrary loans" },
+});
+
+is_deeply(
+    $rq,
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'create',
+        stage   => 'search_results',
+        value   => [
+            {
+                id     => 1234,
+                title  => "Ordering ILLs using Koha",
+                author => "A.N. Other",
+            },
+            {
+                id     => 5678,
+                title  => "Interlibrary loans in Koha",
+                author => "A.N. Other",
+            },
+        ],
+    },
+    "Search_Results stage of create method."
+);
+
+$rq = $backend->create({
+    request    => $mock_request,
+    method     => 'create',
+    stage      => 'commit',
+    other      => {
+        id     => 1234,
+        title  => "Ordering ILLs using Koha",
+        author => "A.N. Other",
+    },
+});
+
+is_deeply(
+    $rq,
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'create',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other"
+        },
+    },
+    "Commit stage of create method."
+);
+
+$rq = $backend->create({
+    request    => $mock_request,
+    method     => 'create',
+    stage      => 'unknown_stage',
+    other      => {
+        id     => 1234,
+        title  => "Ordering ILLs using Koha",
+        author => "A.N. Other",
+    },
+});
+
+is_deeply(
+    $rq,
+    {
+        error   => 1,
+        status  => 'unknown_stage',
+        message => '',
+        method  => 'create',
+        stage   => 'unknown_stage',
+        value   => {},
+    },
+    "Commit stage of create method."
+);
+
+# Test Confirm
+
+$rq = $backend->confirm({
+    request    => $mock_request,
+    other      => undef,
+});
+
+is_deeply(
+    $rq,
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'confirm',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+            status => "On order",
+            cost   => "30 GBP",
+        },
+    },
+    "Basic confirm method."
+);
+
+# Test List
+
+is_deeply(
+    $backend->list,
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'list',
+        stage   => 'commit',
+        value   => {
+            1 => {
+                id     => 1234,
+                title  => "Ordering ILLs using Koha",
+                author => "A.N. Other",
+                status => "On order",
+                cost   => "30 GBP",
+            },
+        },
+    },
+    "Basic list method."
+);
+
+# Test Renew
+
+is_deeply(
+    $backend->renew({
+        request    => $mock_request,
+        other      => undef,
+    }),
+    {
+        error   => 1,
+        status  => 'not_renewed',
+        message => 'Order not yet delivered.',
+        method  => 'renew',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+            status => "On order",
+        },
+    },
+    "Basic renew method."
+);
+
+Koha::Illrequestattributes->find({
+    illrequest_id => $mock_request->illrequest_id,
+    type          => "status"
+})->set({ value => "Delivered" })->store;
+
+is_deeply(
+    $backend->renew({
+        request    => $mock_request,
+        other      => undef,
+    }),
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'renew',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+            status => "Renewed",
+        },
+    },
+    "Modified renew method."
+);
+
+# Test Update_Status
+
+is_deeply(
+    $backend->update_status({
+        request    => $mock_request,
+        other      => undef,
+    }),
+    {
+        error   => 1,
+        status  => 'failed_update_hook',
+        message => 'Fake reason for failing to perform update operation.',
+        method  => 'update_status',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+            status => "Delivered",
+        },
+    },
+    "Basic update_status method."
+);
+
+# FIXME: Perhaps we should add a test checking for specific status code
+# transitions.
+
+# Test Cancel
+
+is_deeply(
+    $backend->cancel({
+        request    => $mock_request,
+        other      => undef,
+    }),
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'cancel',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+            status => "Delivered",
+        },
+    },
+    "Basic cancel method."
+);
+
+is_deeply(
+    $backend->cancel({
+        request    => $mock_request,
+        other      => undef,
+    }),
+    {
+        error   => 1,
+        status  => 'unknown_request',
+        message => 'Cannot cancel an unknown request.',
+        method  => 'cancel',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+        },
+    },
+    "Attempt to cancel an unconfirmed request."
+);
+
+# Test Status
+
+is_deeply(
+    $backend->status({
+        request    => $mock_request,
+        other      => undef,
+    }),
+    {
+        error   => 1,
+        status  => 'unknown_request',
+        message => 'Cannot query status of an unknown request.',
+        method  => 'status',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+        },
+    },
+    "Attempt to get status of an unconfirmed request."
+);
+
+$rq = $backend->confirm({
+    request    => $mock_request,
+    other      => undef,
+});
+
+is_deeply(
+    $backend->status({
+        request    => $mock_request,
+        other      => undef,
+    }),
+    {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'status',
+        stage   => 'commit',
+        value   => {
+            id     => 1234,
+            title  => "Ordering ILLs using Koha",
+            author => "A.N. Other",
+            status => "On order",
+        },
+    },
+    "Basic status method."
+);
+
+1;
diff --git a/t/db_dependent/api/v1/illrequests.t b/t/db_dependent/api/v1/illrequests.t
new file mode 100644
index 0000000..fb306ba
--- /dev/null
+++ b/t/db_dependent/api/v1/illrequests.t
@@ -0,0 +1,136 @@
+#!/usr/bin/env perl
+
+# 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 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.
+#
+# 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.
+
+use Modern::Perl;
+
+use Test::More tests => 1;
+use Test::Mojo;
+use Test::Warn;
+
+use t::lib::TestBuilder;
+use t::lib::Mocks;
+
+use C4::Auth;
+use Koha::Illrequests;
+
+my $schema  = Koha::Database->new->schema;
+my $builder = t::lib::TestBuilder->new;
+
+# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
+# this affects the other REST api tests
+t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
+
+my $remote_address = '127.0.0.1';
+my $t              = Test::Mojo->new('Koha::REST::V1');
+
+subtest 'list() tests' => sub {
+
+    plan tests => 6;
+
+    $schema->storage->txn_begin;
+
+    Koha::Illrequests->search->delete;
+    my ( $borrowernumber, $session_id ) =
+      create_user_and_session( { authorized => 1 } );
+
+    ## Authorized user tests
+    # No requests, so empty array should be returned
+    my $tx = $t->ua->build_tx( GET => '/api/v1/illrequests' );
+    $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
+    $tx->req->env( { REMOTE_ADDR => $remote_address } );
+    $t->request_ok($tx)->status_is(200)->json_is( [] );
+
+#    my $city_country = 'France';
+#    my $city         = $builder->build(
+#        { source => 'City', value => { city_country => $city_country } } );
+#
+#    # One city created, should get returned
+#    $tx = $t->ua->build_tx( GET => '/api/v1/cities' );
+#    $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
+#    $tx->req->env( { REMOTE_ADDR => $remote_address } );
+#    $t->request_ok($tx)->status_is(200)->json_is( [$city] );
+#
+#    my $another_city = $builder->build(
+#        { source => 'City', value => { city_country => $city_country } } );
+#    my $city_with_another_country = $builder->build( { source => 'City' } );
+#
+#    # Two cities created, they should both be returned
+#    $tx = $t->ua->build_tx( GET => '/api/v1/cities' );
+#    $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
+#    $tx->req->env( { REMOTE_ADDR => $remote_address } );
+#    $t->request_ok($tx)->status_is(200)
+#      ->json_is( [ $city, $another_city, $city_with_another_country ] );
+#
+#    # Filtering works, two cities sharing city_country
+#    $tx =
+#      $t->ua->build_tx( GET => "/api/v1/cities?city_country=" . $city_country );
+#    $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
+#    $tx->req->env( { REMOTE_ADDR => $remote_address } );
+#    my $result =
+#      $t->request_ok($tx)->status_is(200)->json_is( [ $city, $another_city ] );
+#
+#    $tx = $t->ua->build_tx(
+#        GET => "/api/v1/cities?city_name=" . $city->{city_name} );
+#    $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
+#    $tx->req->env( { REMOTE_ADDR => $remote_address } );
+#    $t->request_ok($tx)->status_is(200)->json_is( [$city] );
+
+    # Warn on unsupported query parameter
+    $tx = $t->ua->build_tx( GET => '/api/v1/illrequests?request_blah=blah' );
+    $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
+    $tx->req->env( { REMOTE_ADDR => $remote_address } );
+    $t->request_ok($tx)->status_is(400)->json_is(
+        [{ path => '/query/request_blah', message => 'Malformed query string'}]
+    );
+
+    $schema->storage->txn_rollback;
+};
+
+sub create_user_and_session {
+
+    my $args  = shift;
+    my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0;
+    my $dbh   = C4::Context->dbh;
+
+    my $user = $builder->build(
+        {
+            source => 'Borrower',
+            value  => {
+                flags => $flags
+            }
+        }
+    );
+
+    # Create a session for the authorized user
+    my $session = C4::Auth::get_session('');
+    $session->param( 'number',   $user->{borrowernumber} );
+    $session->param( 'id',       $user->{userid} );
+    $session->param( 'ip',       '127.0.0.1' );
+    $session->param( 'lasttime', time() );
+    $session->flush;
+
+    if ( $args->{authorized} ) {
+        $dbh->do( "
+            INSERT INTO user_permissions (borrowernumber,module_bit,code)
+            VALUES (?,3,'parameters_remaining_permissions')", undef,
+            $user->{borrowernumber} );
+    }
+
+    return ( $user->{borrowernumber}, $session->id );
+}
+
+1;
-- 
2.7.4