Bug 32336: (QA follow-up) Use $metadata->schema
[srvgit] / Koha / Illrequest.pm
index 488fa46..d160c15 100644 (file)
@@ -1,35 +1,44 @@
 package Koha::Illrequest;
 
-# Copyright PTFS Europe 2016
+# Copyright PTFS Europe 2016,2018
 #
 # 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 free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
 #
-# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
-# details.
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License along with
-# Koha; if not, write to the Free Software Foundation, Inc., 51 Franklin
-# Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
 
 use Modern::Perl;
 
-use Clone 'clone';
-use File::Basename qw/basename/;
+use Clone qw( clone );
+use Try::Tiny qw( catch try );
+use DateTime;
+
+use C4::Letters;
 use Koha::Database;
-use Koha::Email;
+use Koha::DateUtils qw( dt_from_string );
 use Koha::Exceptions::Ill;
-use Koha::Illrequest;
+use Koha::Illcomments;
 use Koha::Illrequestattributes;
+use Koha::AuthorisedValue;
+use Koha::Illrequest::Logger;
 use Koha::Patron;
-use Mail::Sendmail;
-use Try::Tiny;
+use Koha::AuthorisedValues;
+use Koha::Biblios;
+use Koha::Items;
+use Koha::ItemTypes;
+use Koha::Libraries;
+
+use C4::Circulation qw( CanBookBeIssued AddIssue );
 
 use base qw(Koha::Object);
 
@@ -107,6 +116,60 @@ available for request.
 
 =head2 Class methods
 
+=head3 init_processors
+
+    $request->init_processors()
+
+Initialises an empty processors arrayref
+
+=cut
+
+sub init_processors {
+    my ( $self ) = @_;
+
+    $self->{processors} = [];
+}
+
+=head3 push_processor
+
+    $request->push_processors(sub { ...something... });
+
+Pushes a passed processor function into our processors arrayref
+
+=cut
+
+sub push_processor {
+    my ( $self, $processor ) = @_;
+    push @{$self->{processors}}, $processor;
+}
+
+=head3 statusalias
+
+    my $statusalias = $request->statusalias;
+
+Returns a request's status alias, as a Koha::AuthorisedValue instance
+or implicit undef. This is distinct from status_alias, which only returns
+the value in the status_alias column, this method returns the entire
+AuthorisedValue object
+
+=cut
+
+sub statusalias {
+    my ( $self ) = @_;
+    return unless $self->status_alias;
+    # We can't know which result is the right one if there are multiple
+    # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
+    # so we just use the first
+    return Koha::AuthorisedValues->search(
+        {
+            category         => 'ILL_STATUS_ALIAS',
+            authorised_value => $self->SUPER::status_alias
+        },
+        {},
+        $self->branchcode
+    )->next;
+}
+
 =head3 illrequestattributes
 
 =cut
@@ -118,6 +181,27 @@ sub illrequestattributes {
     );
 }
 
+=head3 illcomments
+
+=cut
+
+sub illcomments {
+    my ( $self ) = @_;
+    return Koha::Illcomments->_new_from_dbic(
+        scalar $self->_result->illcomments
+    );
+}
+
+=head3 logs
+
+=cut
+
+sub logs {
+    my ( $self ) = @_;
+    my $logger = Koha::Illrequest::Logger->new;
+    return $logger->get_request_logs($self);
+}
+
 =head3 patron
 
 =cut
@@ -129,6 +213,117 @@ sub patron {
     );
 }
 
+=head3 status_alias
+
+    $Illrequest->status_alias(143);
+
+Overloaded getter/setter for status_alias,
+that only returns authorised values from the
+correct category and records the fact that the status has changed
+
+=cut
+
+sub status_alias {
+    my ($self, $new_status_alias) = @_;
+
+    my $current_status_alias = $self->SUPER::status_alias;
+
+    if ($new_status_alias) {
+        # Keep a record of the previous status before we change it,
+        # we might need it
+        $self->{previous_status} = $current_status_alias ?
+            $current_status_alias :
+            scalar $self->status;
+        # This is hackery to enable us to undefine
+        # status_alias, since we need to have an overloaded
+        # status_alias method to get us around the problem described
+        # here:
+        # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
+        # We need a way of accepting implied undef, so we can nullify
+        # the status_alias column, when called from $self->status
+        my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
+        my $ret = $self->SUPER::status_alias($val);
+        my $val_to_log = $val ? $new_status_alias : scalar $self->status;
+        if ($ret) {
+            my $logger = Koha::Illrequest::Logger->new;
+            $logger->log_status_change({
+                request => $self,
+                value   => $val_to_log
+            });
+        } else {
+            delete $self->{previous_status};
+        }
+        return $ret;
+    }
+    # We can't know which result is the right one if there are multiple
+    # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
+    # so we just use the first
+    my $alias = Koha::AuthorisedValues->search(
+        {
+            category         => 'ILL_STATUS_ALIAS',
+            authorised_value => $self->SUPER::status_alias
+        },
+        {},
+        $self->branchcode
+    )->next;
+
+    if ($alias) {
+        return $alias->authorised_value;
+    } else {
+        return;
+    }
+}
+
+=head3 status
+
+    $Illrequest->status('CANREQ');
+
+Overloaded getter/setter for request status,
+also nullifies status_alias and records the fact that the status has changed
+and sends a notice if appropriate
+
+=cut
+
+sub status {
+    my ( $self, $new_status) = @_;
+
+    my $current_status = $self->SUPER::status;
+    my $current_status_alias = $self->SUPER::status_alias;
+
+    if ($new_status) {
+        # Keep a record of the previous status before we change it,
+        # we might need it
+        $self->{previous_status} = $current_status_alias ?
+            $current_status_alias :
+            $current_status;
+        my $ret = $self->SUPER::status($new_status)->store;
+        if ($current_status_alias) {
+            # This is hackery to enable us to undefine
+            # status_alias, since we need to have an overloaded
+            # status_alias method to get us around the problem described
+            # here:
+            # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
+            # We need a way of passing implied undef to nullify status_alias
+            # so we pass -1, which is special cased in the overloaded setter
+            $self->status_alias("-1");
+        } else {
+            my $logger = Koha::Illrequest::Logger->new;
+            $logger->log_status_change({
+                request => $self,
+                value   => $new_status
+            });
+        }
+        delete $self->{previous_status};
+        # If status has changed to cancellation requested, send a notice
+        if ($new_status eq 'CANCREQ') {
+            $self->send_staff_notice('ILL_REQUEST_CANCEL');
+        }
+        return $ret;
+    } else {
+        return $current_status;
+    }
+}
+
 =head3 load_backend
 
 Require "Base.pm" from the relevant ILL backend.
@@ -150,7 +345,10 @@ sub load_backend {
     my $location = join "/", @raw, $backend_name, "Base.pm";    # File to load
     my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
     require $location;
-    $self->{_my_backend} = $backend_class->new({ config => $self->_config });
+    $self->{_my_backend} = $backend_class->new({
+        config => $self->_config,
+        logger => Koha::Illrequest::Logger->new
+    });
     return $self;
 }
 
@@ -196,12 +394,15 @@ capabilities & custom_capability and their callers.
 sub _backend_capability {
     my ( $self, $name, $args ) = @_;
     my $capability = 0;
+    # See if capability is defined in backend
     try {
         $capability = $self->_backend->capabilities($name);
     } catch {
+        warn $_;
         return 0;
     };
-    if ( $capability ) {
+    # Try to invoke it
+    if ( $capability && ref($capability) eq 'CODE' ) {
         return &{$capability}($args);
     } else {
         return 0;
@@ -268,7 +469,7 @@ sub _core_status_graph {
             name           => 'Requested',
             ui_method_name => 'Confirm request',
             method         => 'confirm',
-            next_actions   => [ 'REQREV', 'COMP' ],
+            next_actions   => [ 'REQREV', 'COMP', 'CHK' ],
             ui_method_icon => 'fa-check',
         },
         GENREQ => {
@@ -277,7 +478,7 @@ sub _core_status_graph {
             name           => 'Requested from partners',
             ui_method_name => 'Place request with partners',
             method         => 'generic_confirm',
-            next_actions   => [ 'COMP' ],
+            next_actions   => [ 'COMP', 'CHK' ],
             ui_method_icon => 'fa-send-o',
         },
         REQREV => {
@@ -313,7 +514,7 @@ sub _core_status_graph {
             name           => 'Completed',
             ui_method_name => 'Mark completed',
             method         => 'mark_completed',
-            next_actions   => [ ],
+            next_actions   => [ 'CHK' ],
             ui_method_icon => 'fa-check',
         },
         KILL => {
@@ -325,12 +526,34 @@ sub _core_status_graph {
             next_actions   => [ ],
             ui_method_icon => 'fa-trash',
         },
+        CHK => {
+            prev_actions   => [ 'REQ', 'GENREQ', 'COMP' ],
+            id             => 'CHK',
+            name           => 'Checked out',
+            ui_method_name => 'Check out',
+            needs_prefs    => [ 'CirculateILL' ],
+            needs_perms    => [ 'user_circulate_circulate_remaining_permissions' ],
+            # An array of functions that all must return true
+            needs_all      => [ sub { my $r = shift;  return $r->biblio; } ],
+            method         => 'check_out',
+            next_actions   => [ ],
+            ui_method_icon => 'fa-upload',
+        },
+        RET => {
+            prev_actions   => [ 'CHK' ],
+            id             => 'RET',
+            name           => 'Returned to library',
+            ui_method_name => 'Check in',
+            method         => 'check_in',
+            next_actions   => [ 'COMP' ],
+            ui_method_icon => 'fa-download',
+        }
     };
 }
 
-=head3 _core_status_graph
+=head3 _status_graph_union
 
-    my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
+    my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
 
 Return a new status_graph, the result of merging $origin & new_graph.  This is
 operation is a union over the sets defied by the two graphs.
@@ -371,20 +594,22 @@ sub _status_graph_union {
         $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 ) {
+            if ( grep { $prev_action eq $_ } @core_status_ids ) {
                 my @next_actions =
                      @{$status_graph->{$prev_action}->{next_actions}};
-                push @next_actions, $backend_status_key;
+                push @next_actions, $backend_status_key
+                    if (!grep(/^$backend_status_key$/, @next_actions));
                 $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 ) {
+            if ( grep { $next_action eq $_ } @core_status_ids ) {
                 my @prev_actions =
                      @{$status_graph->{$next_action}->{prev_actions}};
-                push @prev_actions, $backend_status_key;
+                push @prev_actions, $backend_status_key
+                    if (!grep(/^$backend_status_key$/, @prev_actions));
                 $status_graph->{$next_action}->{prev_actions}
                     = \@prev_actions;
             }
@@ -476,9 +701,9 @@ Return a list of available backends.
 =cut
 
 sub available_backends {
-    my ( $self ) = @_;
-    my @backends = $self->_config->available_backends;
-    return \@backends;
+    my ( $self, $reduced ) = @_;
+    my $backends = $self->_config->available_backends($reduced);
+    return $backends;
 }
 
 =head3 available_actions
@@ -504,6 +729,7 @@ Mark a request as completed (status = COMP).
 sub mark_completed {
     my ( $self ) = @_;
     $self->status('COMP')->store;
+    $self->completed(dt_from_string())->store;
     return {
         error   => 0,
         status  => '',
@@ -514,6 +740,41 @@ sub mark_completed {
     };
 }
 
+=head2 backend_illview
+
+View and manage an ILL request
+
+=cut
+
+sub backend_illview {
+    my ( $self, $params ) = @_;
+
+    my $response = $self->_backend_capability('illview',{
+        request    => $self,
+        other      => $params,
+    });
+    return $self->expandTemplate($response) if $response;
+    return $response;
+}
+
+=head2 backend_migrate
+
+Migrate a request from one backend to another.
+
+=cut
+
+sub backend_migrate {
+    my ( $self, $params ) = @_;
+    # Set the request's backend to be the destination backend
+    $self->load_backend($params->{backend});
+    my $response = $self->_backend_capability('migrate',{
+            request    => $self,
+            other      => $params,
+        });
+    return $self->expandTemplate($response) if $response;
+    return $response;
+}
+
 =head2 backend_confirm
 
 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
@@ -599,22 +860,25 @@ sub backend_create {
     my ( $self, $params ) = @_;
 
     # Establish whether we need to do a generic copyright clearance.
-    if ( ( !$params->{stage} || $params->{stage} eq 'init' )
-             && C4::Context->preference("ILLModuleCopyrightClearance") ) {
-        return {
-            error   => 0,
-            status  => '',
-            message => '',
-            method  => 'create',
-            stage   => 'copyrightclearance',
-            value   => {
-                backend => $self->_backend->name
-            }
-        };
-    } elsif ( $params->{stage} eq 'copyrightclearance' ) {
-        $params->{stage} = 'init';
+    if ($params->{opac}) {
+        if ( ( !$params->{stage} || $params->{stage} eq 'init' )
+                && C4::Context->preference("ILLModuleCopyrightClearance") ) {
+            return {
+                error   => 0,
+                status  => '',
+                message => '',
+                method  => 'create',
+                stage   => 'copyrightclearance',
+                value   => {
+                    other   => $params,
+                    backend => $self->_backend->name
+                }
+            };
+        } elsif (     defined $params->{stage}
+                && $params->{stage} eq 'copyrightclearance' ) {
+            $params->{stage} = 'init';
+        }
     }
-
     # First perform API action, then...
     my $args = {
         request => $self,
@@ -643,9 +907,45 @@ sub backend_create {
     # ...Updating status!
     $self->status('QUEUED')->store unless ( $permitted );
 
+    ## Handle Unmediated ILLs
+
+    # For the unmediated workflow we only need to delegate to our backend. If
+    # that backend supports unmediateld_ill, it will do its thing and return a
+    # proper response.  If it doesn't then _backend_capability returns 0, so
+    # we keep the current result.
+    if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
+        my $unmediated_result = $self->_backend_capability(
+            'unmediated_ill',
+            $args
+        );
+        $result = $unmediated_result if $unmediated_result;
+    }
+
     return $self->expandTemplate($result);
 }
 
+=head3 backend_get_update
+
+    my $update = backend_get_update($request);
+
+    Given a request, returns an update in a prescribed
+    format that can then be passed to update parsers
+
+=cut
+
+sub backend_get_update {
+    my ( $self, $options ) = @_;
+
+    my $response = $self->_backend_capability(
+        'get_supplier_update',
+        {
+            request => $self,
+            %{$options}
+        }
+    );
+    return $response;
+}
+
 =head3 expandTemplate
 
     my $params = $abstract->expandTemplate($params);
@@ -661,9 +961,9 @@ sub expandTemplate {
     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";
+        ( $params->{method}//q{} ) . ".inc";
     my $opac_tmpl =  join "/", $backend_tmpl, "opac-includes",
-        $params->{method} . ".inc";
+        ( $params->{method}//q{} ) . ".inc";
     # Set files to load
     $params->{template} = $intra_tmpl;
     $params->{opac_template} = $opac_tmpl;
@@ -690,16 +990,19 @@ sub getLimits {
     my ( $self, $params ) = @_;
     my $limits = $self->_config->getLimitRules($params->{type});
 
-    return $limits->{$params->{value}}
-        || $limits->{default}
-        || { count => -1, method => 'active' };
+    if (     defined $params->{value}
+          && defined $limits->{$params->{value}} ) {
+            return $limits->{$params->{value}};
+    }
+    else {
+        return $limits->{default} || { count => -1, method => 'active' };
+    }
 }
 
 =head3 getPrefix
 
     my $prefix = $abstract->getPrefix( {
-        brw_cat => $brw_cat,
-        branch  => $branch_code,
+        branch  => $branch_code
     } );
 
 Return the ILL prefix as defined by our $params: either per borrower category,
@@ -709,15 +1012,25 @@ per branch or the default.
 
 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"
+    my $brn_prefixes = $self->_config->getPrefixes();
+    return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
 }
 
+=head3 get_type
+
+    my $type = $abstract->get_type();
+
+Return a string representing the material type of this request or undef
+
+=cut
+
+sub get_type {
+    my ($self) = @_;
+    my $attr = $self->illrequestattributes->find({ type => 'type'});
+    return if !$attr;
+    return $attr->value;
+};
+
 #### Illrequests Imports
 
 =head3 check_limits
@@ -818,53 +1131,247 @@ sub requires_moderation {
     return $require_moderation->{$self->status};
 }
 
-=head3 generic_confirm
+=head3 biblio
 
-    my $stage_summary = $illRequest->generic_confirm;
+    my $biblio = $request->biblio;
 
-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.
+For a given request, return the biblio associated with it,
+or undef if none exists
 
 =cut
 
-sub generic_confirm {
+sub biblio {
+    my ( $self ) = @_;
+
+    return if !$self->biblio_id;
+
+    return Koha::Biblios->find({
+        biblionumber => $self->biblio_id
+    });
+}
+
+=head3 check_out
+
+    my $stage_summary = $request->check_out;
+
+Handle the check_out method. The first stage involves gathering the required
+data from the user via a form, the second stage creates an item and tries to
+issue it to the patron. If successful, it notifies the patron, then it
+returns a summary of how things went
+
+=cut
+
+sub check_out {
     my ( $self, $params ) = @_;
-    my $branch = Koha::Libraries->find($params->{current_branchcode})
-        || die "Invalid current branchcode. Are you logged in as the database user?";
-    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:
+    # Objects required by the template
+    my $itemtypes = Koha::ItemTypes->search(
+        {},
+        { order_by => ['description'] }
+    );
+    my $libraries = Koha::Libraries->search(
+        {},
+        { order_by => ['branchcode'] }
+    );
+    my $biblio = $self->biblio;
 
-EOF
+    # Find all statistical patrons
+    my $statistical_patrons = Koha::Patrons->search(
+        { 'category_type' => 'x' },
+        { join => { 'categorycode' => 'borrowers' } }
+    );
 
-        my $details = $self->metadata;
-        while (my ($title, $value) = each %{$details}) {
-            $draft->{body} .= "  - " . $title . ": " . $value . "\n"
-                if $value;
+    if (!$params->{stage} || $params->{stage} eq 'init') {
+        # Present a form to gather the required data
+        #
+        # We may be viewing this page having previously tried to issue
+        # the item (in which case, we may already have created an item)
+        # so we pass the biblio for this request
+        return {
+            method  => 'check_out',
+            stage   => 'form',
+            value   => {
+                itemtypes   => $itemtypes,
+                libraries   => $libraries,
+                statistical => $statistical_patrons,
+                biblio      => $biblio
+            }
+        };
+    } elsif ($params->{stage} eq 'form') {
+        # Validate what we've got and return with an error if we fail
+        my $errors = {};
+        if (!$params->{item_type} || length $params->{item_type} == 0) {
+            $errors->{item_type} = 1;
+        }
+        if ($params->{inhouse} && length $params->{inhouse} > 0) {
+            my $patron_count = Koha::Patrons->search({
+                cardnumber => $params->{inhouse}
+            })->count();
+            if ($patron_count != 1) {
+                $errors->{inhouse} = 1;
+            }
         }
-        $draft->{body} .= <<EOF;
 
-Please let us know if you are able to supply this to us.
+        # Check we don't have more than one item for this bib,
+        # if we do, something very odd is going on
+        # Having 1 is OK, it means we're likely trying to issue
+        # following a previously failed attempt, the item exists
+        # so we'll use it
+        my @items = $biblio->items->as_list;
+        my $item_count = scalar @items;
+        if ($item_count > 1) {
+            $errors->{itemcount} = 1;
+        }
 
-Kind Regards
+        # Failed validation, go back to the form
+        if (%{$errors}) {
+            return {
+                method  => 'check_out',
+                stage   => 'form',
+                value   => {
+                    params      => $params,
+                    statistical => $statistical_patrons,
+                    itemtypes   => $itemtypes,
+                    libraries   => $libraries,
+                    biblio      => $biblio,
+                    errors      => $errors
+                }
+            };
+        }
 
-EOF
+        # Passed validation
+        #
+        # Create an item if one doesn't already exist,
+        # if one does, use that
+        my $itemnumber;
+        if ($item_count == 0) {
+            my $item_hash = {
+                biblionumber  => $self->biblio_id,
+                homebranch    => $params->{branchcode},
+                holdingbranch => $params->{branchcode},
+                location      => $params->{branchcode},
+                itype         => $params->{item_type},
+                barcode       => 'ILL-' . $self->illrequest_id
+            };
+            try {
+                my $item = Koha::Item->new($item_hash)->store;
+                $itemnumber = $item->itemnumber;
+            };
+        } else {
+            $itemnumber = $items[0]->itemnumber;
+        }
+        # Check we have an item before going forward
+        if (!$itemnumber) {
+            return {
+                method  => 'check_out',
+                stage   => 'form',
+                value   => {
+                    params      => $params,
+                    itemtypes   => $itemtypes,
+                    libraries   => $libraries,
+                    statistical => $statistical_patrons,
+                    errors      => { item_creation => 1 }
+                }
+            };
+        }
 
-        my @address = map { $branch->$_ }
-            qw/ branchname branchaddress1 branchaddress2 branchaddress3
-                branchzip branchcity branchstate branchcountry branchphone
-                branchemail /;
-        my $address = "";
-        foreach my $line ( @address ) {
-            $address .= $line . "\n" if $line;
+        # Do the check out
+        #
+        # Gather what we need
+        my $target_item = Koha::Items->find( $itemnumber );
+        # Determine who we're issuing to
+        my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
+            Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
+            $self->patron;
+
+        my @issue_args = (
+            $patron,
+            scalar $target_item->barcode
+        );
+        if ($params->{duedate} && length $params->{duedate} > 0) {
+            push @issue_args, dt_from_string($params->{duedate});
         }
+        # Check if we can check out
+        my ( $error, $confirm, $alerts, $messages ) =
+            C4::Circulation::CanBookBeIssued(@issue_args);
+
+        # If we got anything back saying we can't check out,
+        # return it to the template
+        my $problems = {};
+        if ( $error && %{$error} ) { $problems->{error} = $error };
+        if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
+        if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
+        if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
+
+        if (%{$problems}) {
+            return {
+                method  => 'check_out',
+                stage   => 'form',
+                value   => {
+                    params           => $params,
+                    itemtypes        => $itemtypes,
+                    libraries        => $libraries,
+                    statistical      => $statistical_patrons,
+                    patron           => $patron,
+                    biblio           => $biblio,
+                    check_out_errors => $problems
+                }
+            };
+        }
+
+        # We can allegedly check out, so make it so
+        # For some reason, AddIssue requires an unblessed Patron
+        $issue_args[0] = $patron->unblessed;
+        my $issue = C4::Circulation::AddIssue(@issue_args);
+
+        if ($issue) {
+            # Update the request status
+            $self->status('CHK')->store;
+            return {
+                method  => 'check_out',
+                stage   => 'done_check_out',
+                value   => {
+                    params    => $params,
+                    patron    => $patron,
+                    check_out => $issue
+                }
+            };
+        } else {
+            return {
+                method  => 'check_out',
+                stage   => 'form',
+                value   => {
+                    params    => $params,
+                    itemtypes => $itemtypes,
+                    libraries => $libraries,
+                    errors    => { item_check_out => 1 }
+                }
+            };
+        }
+    }
+
+}
+
+=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
 
-        $draft->{body} .= $address;
+sub generic_confirm {
+    my ( $self, $params ) = @_;
+    my $branch = Koha::Libraries->find($params->{current_branchcode})
+        || die "Invalid current branchcode. Are you logged in as the database user?";
+    if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
+        # Get the message body from the notice definition
+        my $letter = $self->get_notice({
+            notice_code => 'ILL_PARTNER_REQ',
+            transport   => 'email'
+        });
 
         my $partners = Koha::Patrons->search({
             categorycode => $self->_config->partner_code
@@ -876,7 +1383,10 @@ EOF
             method  => 'generic_confirm',
             stage   => 'draft',
             value   => {
-                draft    => $draft,
+                draft => {
+                    subject => $letter->{title},
+                    body    => $letter->{content}
+                },
                 partners => $partners,
             }
         };
@@ -884,52 +1394,321 @@ EOF
     } 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 );
+        if ( defined $to ) {
+            $to =~ s/^\x00//;       # Strip leading NULLs
+        }
+        Koha::Exceptions::Ill::NoTargetEmail->throw(
+            "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
+          if ( !$to );
+
+        # Take the null delimited string that we receive and create
+        # an array of associated patron objects
+        my @to_patrons = map {
+            Koha::Patrons->find({ borrowernumber => $_ })
+        } split(/\x00/, $to);
+
         # Create the from, replyto and sender headers
-        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',
+        my $from = $branch->from_email_address;
+        my $replyto = $branch->inbound_ill_address;
+        Koha::Exceptions::Ill::NoLibraryEmail->throw(
+            "Your library has no usable email address. Please set it.")
+          if ( !$from );
+
+        # So we get a notice hashref, then substitute the possibly
+        # modified title and body from the draft stage
+        my $letter = $self->get_notice({
+            notice_code => 'ILL_PARTNER_REQ',
+            transport   => 'email'
+        });
+        $letter->{title} = $params->{subject};
+        $letter->{content} = $params->{body};
+
+        if ($letter) {
+
+            # Keep track of who received this notice
+            my @queued = ();
+            # Iterate our array of recipient patron objects
+            foreach my $patron(@to_patrons) {
+                # Create the params we pass to the notice
+                my $params = {
+                    letter                 => $letter,
+                    borrowernumber         => $patron->borrowernumber,
+                    message_transport_type => 'email',
+                    to_address             => $patron->email,
+                    from_address           => $from,
+                    reply_address          => $replyto
+                };
+                my $result = C4::Letters::EnqueueLetter($params);
+                if ( $result ) {
+                    push @queued, $patron->email;
+                }
             }
-        );
-        # 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',
-            };
+
+            # If all notices were queued successfully,
+            # store that
+            if (scalar @queued == scalar @to_patrons) {
+                $self->status("GENREQ")->store;
+                $self->_backend_capability(
+                    'set_requested_partners',
+                    {
+                        request => $self,
+                        to => join("; ", @queued)
+                    }
+                );
+                return {
+                    error   => 0,
+                    status  => '',
+                    message => '',
+                    method  => 'generic_confirm',
+                    stage   => 'commit',
+                    next    => 'illview',
+                };
+            }
+
         }
+        return {
+            error   => 1,
+            status  => 'email_failed',
+            message => 'Email queueing failed',
+            method  => 'generic_confirm',
+            stage   => 'draft',
+        };
     } else {
         die "Unknown stage, should not have happened."
     }
 }
 
+=head3 send_patron_notice
+
+    my $result = $request->send_patron_notice($notice_code);
+
+Send a specified notice regarding this request to a patron
+
+=cut
+
+sub send_patron_notice {
+    my ( $self, $notice_code, $additional_text ) = @_;
+
+    # We need a notice code
+    if (!$notice_code) {
+        return {
+            error => 'notice_no_type'
+        };
+    }
+
+    # Map from the notice code to the messaging preference
+    my %message_name = (
+        ILL_PICKUP_READY    => 'Ill_ready',
+        ILL_REQUEST_UNAVAIL => 'Ill_unavailable',
+        ILL_REQUEST_UPDATE  => 'Ill_update'
+    );
+
+    # Get the patron's messaging preferences
+    my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
+        borrowernumber => $self->borrowernumber,
+        message_name   => $message_name{$notice_code}
+    });
+    my @transports = keys %{ $borrower_preferences->{transports} };
+
+    # Notice should come from the library where the request was placed,
+    # not the patrons home library
+    my $branch = Koha::Libraries->find($self->branchcode);
+    my $from_address = $branch->from_email_address;
+    my $reply_address = $branch->inbound_ill_address;
+
+    # Send the notice to the patron via the chosen transport methods
+    # and record the results
+    my @success = ();
+    my @fail = ();
+    for my $transport (@transports) {
+        my $letter = $self->get_notice({
+            notice_code     => $notice_code,
+            transport       => $transport,
+            additional_text => $additional_text
+        });
+        if ($letter) {
+            my $result = C4::Letters::EnqueueLetter({
+                letter                 => $letter,
+                borrowernumber         => $self->borrowernumber,
+                message_transport_type => $transport,
+                from_address           => $from_address,
+                reply_address          => $reply_address
+            });
+            if ($result) {
+                push @success, $transport;
+            } else {
+                push @fail, $transport;
+            }
+        } else {
+            push @fail, $transport;
+        }
+    }
+    if (scalar @success > 0) {
+        my $logger = Koha::Illrequest::Logger->new;
+        $logger->log_patron_notice({
+            request => $self,
+            notice_code => $notice_code
+        });
+    }
+    return {
+        result => {
+            success => \@success,
+            fail    => \@fail
+        }
+    };
+}
+
+=head3 send_staff_notice
+
+    my $result = $request->send_staff_notice($notice_code);
+
+Send a specified notice regarding this request to staff
+
+=cut
+
+sub send_staff_notice {
+    my ( $self, $notice_code ) = @_;
+
+    # We need a notice code
+    if (!$notice_code) {
+        return {
+            error => 'notice_no_type'
+        };
+    }
+
+    # Get the staff notices that have been assigned for sending in
+    # the syspref
+    my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
+
+    # If it hasn't been enabled in the syspref, we don't want to send it
+    if ($staff_to_send !~ /\b$notice_code\b/) {
+        return {
+            error => 'notice_not_enabled'
+        };
+    }
+
+    my $letter = $self->get_notice({
+        notice_code => $notice_code,
+        transport   => 'email'
+    });
+
+    # Try and get an address to which to send staff notices
+    my $branch = Koha::Libraries->find($self->branchcode);
+    my $to_address = $branch->inbound_ill_address;
+    my $from_address = $branch->inbound_ill_address;
+
+    my $params = {
+        letter                 => $letter,
+        borrowernumber         => $self->borrowernumber,
+        message_transport_type => 'email',
+        from_address           => $from_address
+    };
+
+    if ($to_address) {
+        $params->{to_address} = $to_address;
+    } else {
+        return {
+            error => 'notice_no_create'
+        };
+    }
+
+    if ($letter) {
+        C4::Letters::EnqueueLetter($params)
+            or warn "can't enqueue letter $letter";
+        return {
+            success => 'notice_queued'
+        };
+    } else {
+        return {
+            error => 'notice_no_create'
+        };
+    }
+}
+
+=head3 get_notice
+
+    my $notice = $request->get_notice($params);
+
+Return a compiled notice hashref for the passed notice code
+and transport type
+
+=cut
+
+sub get_notice {
+    my ( $self, $params ) = @_;
+
+    my $title = $self->illrequestattributes->find(
+        { type => 'title' }
+    );
+    my $author = $self->illrequestattributes->find(
+        { type => 'author' }
+    );
+    my $metahash = $self->metadata;
+    my @metaarray = ();
+    foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
+        my $value = $metahash->{$key};
+        push @metaarray, "- $key: $value" if $value;
+    }
+    my $metastring = join("\n", @metaarray);
+    my $letter = C4::Letters::GetPreparedLetter(
+        module                 => 'ill',
+        letter_code            => $params->{notice_code},
+        branchcode             => $self->branchcode,
+        message_transport_type => $params->{transport},
+        lang                   => $self->patron->lang,
+        tables                 => {
+            illrequests => $self->illrequest_id,
+            borrowers   => $self->borrowernumber,
+            biblio      => $self->biblio_id,
+            branches    => $self->branchcode,
+        },
+        substitute  => {
+            ill_bib_title      => $title ? $title->value : '',
+            ill_bib_author     => $author ? $author->value : '',
+            ill_full_metadata  => $metastring,
+            additional_text    => $params->{additional_text}
+        }
+    );
+
+    return $letter;
+}
+
+
+=head3 attach_processors
+
+Receive a Koha::Illrequest::SupplierUpdate and attach
+any processors we have for it
+
+=cut
+
+sub attach_processors {
+    my ( $self, $update ) = @_;
+
+    foreach my $processor(@{$self->{processors}}) {
+        if (
+            $processor->{target_source_type} eq $update->{source_type} &&
+            $processor->{target_source_name} eq $update->{source_name}
+        ) {
+            $update->attach_processor($processor);
+        }
+    }
+}
+
+=head3 append_to_note
+
+    append_to_note("Some text");
+
+Append some text to the staff note
+
+=cut
+
+sub append_to_note {
+    my ($self, $text) = @_;
+    my $current = $self->notesstaff;
+    $text = ($current && length $current > 0) ? "$current\n\n$text" : $text;
+    $self->notesstaff($text)->store;
+}
+
 =head3 id_prefix
 
     my $prefix = $record->id_prefix;
@@ -942,12 +1721,7 @@ file.
 
 sub id_prefix {
     my ( $self ) = @_;
-    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 );
@@ -972,6 +1746,71 @@ sub _censor {
     return $params;
 }
 
+=head3 store
+
+    $Illrequest->store;
+
+Overloaded I<store> method that, in addition to performing the 'store',
+possibly records the fact that something happened
+
+=cut
+
+sub store {
+    my ( $self, $attrs ) = @_;
+
+    my %updated_columns = $self->_result->get_dirty_columns;
+
+    my @holds;
+    if( $self->in_storage and defined $updated_columns{'borrowernumber'} and
+        Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
+    {
+        # borrowernumber has changed
+        my $old_illreq = $self->get_from_storage;
+        @holds = Koha::Holds->search( {
+            borrowernumber => $old_illreq->borrowernumber,
+            biblionumber   => $self->biblio_id,
+        } )->as_list if $old_illreq;
+    }
+
+    my $ret = $self->SUPER::store;
+
+    if ( scalar @holds ) {
+        # move holds to the changed borrowernumber
+        foreach my $hold ( @holds ) {
+            $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
+        }
+    }
+
+    $attrs->{log_origin} = 'core';
+
+    if ($ret && defined $attrs) {
+        my $logger = Koha::Illrequest::Logger->new;
+        $logger->log_maybe({
+            request => $self,
+            attrs   => $attrs
+        });
+    }
+
+    return $ret;
+}
+
+=head3 requested_partners
+
+    my $partners_string = $illRequest->requested_partners;
+
+Return the string representing the email addresses of the partners to
+whom a request has been sent
+
+=cut
+
+sub requested_partners {
+    my ( $self ) = @_;
+    return $self->_backend_capability(
+        'get_requested_partners',
+        { request => $self }
+    );
+}
+
 =head3 TO_JSON
 
     $json = $illrequest->TO_JSON
@@ -979,39 +1818,16 @@ sub _censor {
 Overloaded I<TO_JSON> method that takes care of inserting calculated values
 into the unblessed representation of the object.
 
+TODO: This method does nothing and is not called anywhere. However, bug 74325
+touches it, so keeping this for now until both this and bug 74325 are merged,
+at which point we can sort it out and remove it completely
+
 =cut
 
 sub TO_JSON {
     my ( $self, $embed ) = @_;
 
     my $object = $self->SUPER::TO_JSON();
-    $object->{id_prefix} = $self->id_prefix;
-
-    if ( scalar (keys %$embed) ) {
-        # Augment the request response with patron details if appropriate
-        if ( $embed->{patron} ) {
-            my $patron = $self->patron;
-            $object->{patron} = {
-                firstname  => $patron->firstname,
-                surname    => $patron->surname,
-                cardnumber => $patron->cardnumber
-            };
-        }
-        # Augment the request response with metadata details if appropriate
-        if ( $embed->{metadata} ) {
-            $object->{metadata} = $self->metadata;
-        }
-        # Augment the request response with status details if appropriate
-        if ( $embed->{capabilities} ) {
-            $object->{capabilities} = $self->capabilities;
-        }
-        # Augment the request response with library details if appropriate
-        if ( $embed->{branch} ) {
-            $object->{branch} = Koha::Libraries->find(
-                $self->branchcode
-            )->TO_JSON;
-        }
-    }
 
     return $object;
 }
@@ -1029,6 +1845,7 @@ sub _type {
 =head1 AUTHOR
 
 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
 
 =cut